From d4146df34f957c899b49dc463e2bb057ba492bd6 Mon Sep 17 00:00:00 2001 From: Jonathan Sebastian Date: Tue, 21 May 2024 18:49:16 -0700 Subject: [PATCH] Modbus simulator - Added support for additional datatypes This update adds native support for some of the most commonly found datatypes in OT systems. This includes support for bitfields, unsigned integers, signed integers, and floating point numbers in multiple word sizes (16 bit, 32 bit and 64 bit). Relevant changes include: A reworked version of simulator.py that relies on struct.unpack, struct.pack, and other native functions to simplifly datatype handling while also incorportating additional checks to ensure the register layout behaves as expected. In order to simplifly future work development, the Bits field has been renamed bitfield16, while also adding support for 32 and 64 bit fields. To help facilitate adoption, several changes have been done to the project files to ensure a smooth transition. This includes: - Updated documentation to reflect new datatypes, including examples. - Updated unit tests, to ensure the code works as expected. - Updated the simulator example to illustrate how the new datatypes can be used - Performed linting on 3.12 Resolves [discussion]: #1458 Partially addresses: #1284 --- doc/source/library/simulator/config.rst | 110 ++++- examples/datastore_simulator_share.py | 189 ++++++--- examples/simulator.py | 2 +- pymodbus/datastore/simulator.py | 518 +++++++++++++++++++----- pymodbus/server/simulator/setup.json | 75 +++- test/sub_server/test_simulator.py | 146 +++++-- 6 files changed, 801 insertions(+), 239 deletions(-) diff --git a/doc/source/library/simulator/config.rst b/doc/source/library/simulator/config.rst index 700f9a89f..17f3376bb 100644 --- a/doc/source/library/simulator/config.rst +++ b/doc/source/library/simulator/config.rst @@ -331,19 +331,28 @@ Registers can be singulars (first entry) or arrays (second entry) Bits section ^^^^^^^^^^^^ -Example "bits" configuration: +Breaking change, bits have now been renamed into bitfield16, bitfield32 and bitfield64 to promote consistency + + + + +BitField16 section +^^^^^^^^^^^^^^ +Replaces "bits" + +Example "bitfield16" configuration: .. code-block:: - "bits": [ - 5, - [6, 7], + "bitfield16": [ + 6 + [6], {"addr": 8, "value": 7}, - {"addr": 9, "value": 7, "action": "random"}, - {"addr": [11, 12], "value": 7, "action": "random"} + {"addr": 9, "value": 3, "action": "increment"}, + {"addr": [11], "value": 1, "action": "random"} ], -defines registers which contain bits (discrete input and coils), +Defines registers which contain a 16 bit field, they are effectively sintactic sugar for Uint16 Registers can be singulars (first entry) or arrays (second entry), furthermore a value and/or a action can be defined, @@ -351,6 +360,50 @@ the value and/or action is inserted into each register defined in "addr". +BitField32 section +^^^^^^^^^^^^^^ + +Example "bitfield32" configuration: + +.. code-block:: + + "bitfield32": [ + [6, 7], + {"addr": [8, 9], "value": 31}, + {"addr": [10, 13], "value": 255, "action": "increment"}, + {"addr": [14, 15], "value": 65535, "action": "random"} + ], + +Defines registers which contain a 32 bit field, they are effectively sintactic sugar for Uint32 + +Registers can only be arrays in multiples of 2, +furthermore a value and/or a action can be defined, +the value and/or action is converted (high/low value) and inserted into each register set defined in "addr". + + +Uint64 section +^^^^^^^^^^^^^^ + +Example "uint64" configuration: + +.. code-block:: + + "uint64": [ + [6, 9], + {"addr": [8, 11], "value": 18446744073709551615}, + {"addr": [12, 15], "value": 255, "action": "increment"}, + {"addr": [16, 119], "value": 7, "action": "random"} + ], + +Defines registers which contain a 64 bit field, they are effectively sintactic sugar for Uint64 + +Registers can only be arrays in multiples of 4, +furthermore a value and/or a action can be defined, +the value and/or action is converted (high/low value) and inserted into each register set defined in "addr". + + + + Uint16 section ^^^^^^^^^^^^^^ @@ -395,6 +448,26 @@ furthermore a value and/or a action can be defined, the value and/or action is converted (high/low value) and inserted into each register set defined in "addr". +Uint64 section +^^^^^^^^^^^^^^ + +Example "uint64" configuration: + +.. code-block:: + + "uint64": [ + [6, 9], + {"addr": [8, 11], "value": 30012300}, + {"addr": [12, 15], "value": 40071200, "action": "increment"}, + {"addr": [16, 119], "value": 50051700, "action": "random"} + ], + +defines sets of registers (4) which contain a 64 bit unsigned integer, + +Registers can only be arrays in multiples of 4, +furthermore a value and/or a action can be defined, +the value and/or action is converted (high/low value) and inserted into each register set defined in "addr". + Float32 section ^^^^^^^^^^^^^^^ @@ -419,6 +492,29 @@ the value and/or action is converted (high/low value) and inserted into each reg Remark remember to set ``"value": `` like 512.0 (float) not 512 (integer). +Float64 section +^^^^^^^^^^^^^^^ + +Example "float64" configuration: + +.. code-block:: + + "float64": [ + [6, 9], + {"addr": [8, 11], "value": 3123.17}, + {"addr": [12, 15], "value": 712.5, "action": "increment"}, + {"addr": [16, 20], "value": 517.0, "action": "random"} + ], + +defines sets of registers (4) which contain a 64 bit float, + +Registers can only be arrays in multiples of 4, +furthermore a value and/or a action can be defined, +the value and/or action is converted (high/low value) and inserted into each register set defined in "addr". + +Remark remember to set ``"value": `` like 512.0 (float) not 512 (integer). + + String section ^^^^^^^^^^^^^^ diff --git a/examples/datastore_simulator_share.py b/examples/datastore_simulator_share.py index cc6eae19f..2971328ff 100755 --- a/examples/datastore_simulator_share.py +++ b/examples/datastore_simulator_share.py @@ -41,74 +41,131 @@ _logger = logging.getLogger(__file__) demo_config = { - "setup": { - "co size": 100, - "di size": 150, - "hr size": 200, - "ir size": 250, - "shared blocks": True, - "type exception": False, - "defaults": { - "value": { - "bits": 0x0708, - "uint16": 1, - "uint32": 45000, - "float32": 127.4, - "string": "X", - }, - "action": { - "bits": None, - "uint16": None, - "uint32": None, - "float32": None, - "string": None, + "setup": { + "co size": 100, + "di size": 150, + "hr size": 200, + "ir size": 300, + "shared blocks": True, + "type exception": False, + "defaults": { + "value": { + "bitfield16": 0x0708, + "bitfield32": 0x10010708, + "bitfield64": 0x8001000000003708, + "int16": -1, + "int32": -45000, + "int64": -450000000, + "uint16": 1, + "uint32": 45000, + "uint64": 450000000, + "float32": 127.4, + "float64": 10127.4, + "string": "X", + }, + "action": { + "bitfield16": None, + "bitfield32": None, + "bitfield64": None, + "int16": None, + "int32": None, + "int64": None, + "uint16": None, + "uint32": None, + "uint64": None, + "float32": None, + "float64": None, + "string": None, + }, }, }, - }, - "invalid": [ - 1, - [6, 6], - ], - "write": [ - 3, - [7, 8], - [16, 18], - [21, 26], - [31, 36], - ], - "bits": [ - [7, 9], - {"addr": 2, "value": 0x81}, - {"addr": 3, "value": 17}, - {"addr": 4, "value": 17}, - {"addr": 5, "value": 17}, - {"addr": 10, "value": 0x81}, - {"addr": [11, 12], "value": 0x04342}, - {"addr": 13, "action": "reset"}, - {"addr": 14, "value": 15, "action": "reset"}, - ], - "uint16": [ - {"addr": 16, "value": 3124}, - {"addr": [17, 18], "value": 5678}, - {"addr": [19, 20], "value": 14661, "action": "increment"}, - ], - "uint32": [ - {"addr": [21, 22], "value": 3124}, - {"addr": [23, 26], "value": 5678}, - {"addr": [27, 30], "value": 345000, "action": "increment"}, - ], - "float32": [ - {"addr": [31, 32], "value": 3124.17}, - {"addr": [33, 36], "value": 5678.19}, - {"addr": [37, 40], "value": 345000.18, "action": "increment"}, - ], - "string": [ - {"addr": [41, 42], "value": "Str"}, - {"addr": [43, 44], "value": "Strx"}, - ], - "repeat": [{"addr": [0, 45], "to": [46, 138]}], -} - + "invalid": [ + 1, + [3, 4], + ], + "write": [ + 5, + [7, 8], + [16, 18], + [21, 26], + [33, 38], + ], + "bitfield16": [ + [7, 7], + [8, 8], + {"addr": 2, "value": 0x81}, + {"addr": 3, "value": 17}, + {"addr": 4, "value": 17}, + {"addr": 5, "value": 17}, + {"addr": 10, "value": 0x81}, + {"addr": [11, 11], "value": 0x04342}, + {"addr": [12, 12], "value": 0x04342}, + {"addr": 13, "action": "random"}, + {"addr": 14, "value": 15, "action": "reset"}, + ], + "bitfield32": [ + [50, 51], + {"addr": [52,53], "value": 0x04342}, + ], + "bitfield64": [ + [54, 57], + {"addr": [58,61], "value": 0x04342}, + ], + "int16": [ + 70, + [71, 71], + {"addr": 72, "value": 0x81}, + {"addr": [73, 73], "value": 0x04342}, + {"addr": 74, "action": "random"}, + {"addr": 75, "value": 15, "action": "reset"}, + ], + "int32": [ + [76, 77], + {"addr": [78,79], "value": 0x04342}, + ], + "int64": [ + [80, 83], + {"addr": [84,87], "value": 0x04342}, + ], + "uint16": [ + {"addr": 16, "value": 3124}, + {"addr": [17, 18], "value": 5678}, + { + "addr": [19, 20], + "value": 14661, + "action": "increment", + "args": {"minval": 1, "maxval": 100}, + }, + ], + "uint32": [ + {"addr": [21, 22], "value": 3124}, + {"addr": [23, 26], "value": 5678}, + {"addr": [27, 30], "value": 345000, "action": "increment"}, + { + "addr": [31, 32], + "value": 50, + "action": "random", + "kwargs": {"minval": 10, "maxval": 80}, + }, + ], + "uint64": [ + {"addr": [62, 65], "value": 3124} + ], + "float32": [ + {"addr": [33, 34], "value": 3124.5}, + {"addr": [35, 38], "value": 5678.19}, + {"addr": [39, 42], "value": 345000.18, "action": "increment"}, + ], + "float64": [ + {"addr": [66, 69], "value": 3124.5}, + ], + "string": [ + {"addr": [43, 44], "value": "Str"}, + {"addr": [45, 48], "value": "Strxyz12"}, + ], + "repeat": [{"addr": [0, 95], "to": [96, 191]}, + {"addr": [0, 95], "to": [192, 287]}], + } def custom_action1(_inx, _cell): """Test action.""" diff --git a/examples/simulator.py b/examples/simulator.py index 58f7ce6fb..1549b6035 100755 --- a/examples/simulator.py +++ b/examples/simulator.py @@ -28,7 +28,7 @@ async def read_registers( if count == 1: value = rr.registers[0] else: - value = ModbusSimulatorContext.build_value_from_registers(rr.registers, is_int) + value = ModbusSimulatorContext.build_value_from_registers(rr.registers, is_int,4,False) if not is_int: value = round(value, 1) if curval: diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py index 31b61d856..b3e48a820 100644 --- a/pymodbus/datastore/simulator.py +++ b/pymodbus/datastore/simulator.py @@ -19,12 +19,19 @@ class CellType: """Define single cell types.""" INVALID: int = 0 - BITS: int = 1 - UINT16: int = 2 - UINT32: int = 3 - FLOAT32: int = 4 - STRING: int = 5 - NEXT: int = 6 + BITFIELD16: int = 1 + BITFIELD32: int = 2 + BITFIELD64: int = 3 + INT16: int = 4 + INT32: int = 5 + INT64: int = 6 + UINT16: int = 7 + UINT32: int = 8 + UINT64: int = 9 + FLOAT32: int = 10 + FLOAT64: int = 11 + STRING: int = 12 + NEXT: int = 13 @dataclasses.dataclass(repr=False, eq=False) @@ -81,11 +88,18 @@ class Label: # pylint: disable=too-many-instance-attributes timestamp: str = "timestamp" repeat_to: str = "to" type: str = "type" - type_bits = "bits" + type_bitfield16 = "bitfield16" + type_bitfield32 = "bitfield32" + type_bitfield64 = "bitfield64" type_exception: str = "type exception" + type_int16: str = "int16" + type_int32: str = "int32" + type_int64: str = "int64" type_uint16: str = "uint16" type_uint32: str = "uint32" + type_uint64: str = "uint64" type_float32: str = "float32" + type_float64: str = "float64" type_string: str = "string" uptime: str = "uptime" value: str = "value" @@ -111,12 +125,47 @@ def __init__(self, runtime): self.runtime = runtime self.config = {} self.config_types: dict[str, dict[str, Any]] = { - Label.type_bits: { - Label.type: CellType.BITS, + Label.type_bitfield16: { + Label.type: CellType.BITFIELD16, Label.next: None, Label.value: 0, Label.action: None, - Label.method: self.handle_type_bits, + Label.method: self.handle_type_bitfield16, + }, + Label.type_bitfield32: { + Label.type: CellType.BITFIELD32, + Label.next: CellType.NEXT, + Label.value: 0, + Label.action: None, + Label.method: self.handle_type_bitfield32, + }, + Label.type_bitfield64: { + Label.type: CellType.BITFIELD64, + Label.next: CellType.NEXT, + Label.value: 0, + Label.action: None, + Label.method: self.handle_type_bitfield64, + }, + Label.type_int16: { + Label.type: CellType.INT16, + Label.next: None, + Label.value: 0, + Label.action: None, + Label.method: self.handle_type_int16, + }, + Label.type_int32: { + Label.type: CellType.INT32, + Label.next: CellType.NEXT, + Label.value: 0, + Label.action: None, + Label.method: self.handle_type_int32, + }, + Label.type_int64: { + Label.type: CellType.INT64, + Label.next: CellType.NEXT, + Label.value: 0, + Label.action: None, + Label.method: self.handle_type_int64, }, Label.type_uint16: { Label.type: CellType.UINT16, @@ -132,6 +181,13 @@ def __init__(self, runtime): Label.action: None, Label.method: self.handle_type_uint32, }, + Label.type_uint64: { + Label.type: CellType.UINT64, + Label.next: CellType.NEXT, + Label.value: 0, + Label.action: None, + Label.method: self.handle_type_uint64, + }, Label.type_float32: { Label.type: CellType.FLOAT32, Label.next: CellType.NEXT, @@ -139,6 +195,13 @@ def __init__(self, runtime): Label.action: None, Label.method: self.handle_type_float32, }, + Label.type_float64: { + Label.type: CellType.FLOAT64, + Label.next: CellType.NEXT, + Label.value: 0, + Label.action: None, + Label.method: self.handle_type_float64, + }, Label.type_string: { Label.type: CellType.STRING, Label.next: CellType.NEXT, @@ -148,53 +211,99 @@ def __init__(self, runtime): }, } - def handle_type_bits(self, start, stop, value, action, action_kwargs): - """Handle type bits.""" - for reg in self.runtime.registers[start:stop]: - if reg.type != CellType.INVALID: - raise RuntimeError(f'ERROR "{Label.type_bits}" {reg} used') - reg.value = value - reg.type = CellType.BITS - reg.action = action - reg.action_kwargs = action_kwargs + def handle_type_2Bytes(self, start, stop, regs_value, action, action_kwargs,cell_type): + """Handle datatypes that require 2 bytes in order to be represented, e.g., 1 register.""" + for i in range(start, stop, 1): + regs = self.runtime.registers[i : i+1] + assert len(regs)==1,"Failed assertion" + if regs[0].type != CellType.INVALID : + raise RuntimeError(f'ERROR "{cell_type}" {i} is already being used') + + regs[0].action = action + regs[0].action_kwargs = action_kwargs + for ix in range (0,1): + regs[ix].value = regs_value[ix] + regs[ix].type = CellType.NEXT #Reg0 will be rewritten next + regs[0].type = cell_type + + def handle_type_4Bytes(self, start, stop, regs_value, action, action_kwargs,cell_type): + """Handle datatypes that require 4 bytes in order to be represented, e.g., 2 registers.""" + for i in range(start, stop, 2): + regs = self.runtime.registers[i : i + 2] + if regs[0].type != CellType.INVALID or regs[1].type != CellType.INVALID: + raise RuntimeError(f'ERROR "{cell_type}" {i},{i + 1} is already being used') + + regs[0].action = action + regs[0].action_kwargs = action_kwargs + for ix in range (0,2): + regs[ix].value = regs_value[ix] + regs[ix].type = CellType.NEXT #Reg0 will be rewritten next + regs[0].type = cell_type + + def handle_type_8Bytes(self, start, stop, regs_value, action, action_kwargs,cell_type): + """Handle datatypes that require 8 bytes in order to be represented, e.g., 4 registers.""" + for i in range(start, stop, 4): + regs = self.runtime.registers[i : i + 4] + if regs[0].type != CellType.INVALID or regs[1].type != CellType.INVALID or regs[2].type != CellType.INVALID or regs[3].type != CellType.INVALID: + raise RuntimeError(f'ERROR "{cell_type}" {i},{i + 1},{i + 2},{i + 3} is already being used') + regs[0].action = action + regs[0].action_kwargs = action_kwargs + for ix in range (0,4): + regs[ix].value = regs_value[ix] + regs[ix].type = CellType.NEXT #Reg0 will be rewritten next + regs[0].type = cell_type + def handle_type_bitfield16(self, start, stop, value, action, action_kwargs): + """Handle type bits16.""" + regs_value = ModbusSimulatorContext.build_registers_from_value(value, True,2,False) + self.handle_type_2Bytes( start, stop, regs_value, action, action_kwargs,CellType.BITFIELD16) + def handle_type_bitfield32(self, start, stop, value, action, action_kwargs): + """Handle type bits32.""" + regs_value = ModbusSimulatorContext.build_registers_from_value(value, True,4,False) + self.handle_type_4Bytes( start, stop, regs_value, action, action_kwargs,CellType.BITFIELD32) + def handle_type_bitfield64(self, start, stop, value, action, action_kwargs): + """Handle type bits64.""" + regs_value = ModbusSimulatorContext.build_registers_from_value(value, True,8,False) + self.handle_type_8Bytes( start, stop, regs_value, action, action_kwargs,CellType.BITFIELD64) + + def handle_type_int16(self, start, stop, value, action, action_kwargs): + """Handle type int16.""" + regs_value = ModbusSimulatorContext.build_registers_from_value(value, True,2,True) + self.handle_type_2Bytes( start, stop, regs_value, action, action_kwargs,CellType.INT16) + + def handle_type_int32(self, start, stop, value, action, action_kwargs): + """Handle type int32.""" + regs_value = ModbusSimulatorContext.build_registers_from_value(value, True,4,True) + self.handle_type_4Bytes( start, stop, regs_value, action, action_kwargs,CellType.INT32) + + def handle_type_int64(self, start, stop, value, action, action_kwargs): + """Handle type int64.""" + regs_value = ModbusSimulatorContext.build_registers_from_value(value, True,8,True) + self.handle_type_8Bytes( start, stop, regs_value, action, action_kwargs,CellType.INT64) def handle_type_uint16(self, start, stop, value, action, action_kwargs): """Handle type uint16.""" - for reg in self.runtime.registers[start:stop]: - if reg.type != CellType.INVALID: - raise RuntimeError(f'ERROR "{Label.type_uint16}" {reg} used') - reg.value = value - reg.type = CellType.UINT16 - reg.action = action - reg.action_kwargs = action_kwargs + regs_value = ModbusSimulatorContext.build_registers_from_value(value, True,2,False) + self.handle_type_2Bytes( start, stop, regs_value, action, action_kwargs,CellType.UINT16) def handle_type_uint32(self, start, stop, value, action, action_kwargs): """Handle type uint32.""" - regs_value = ModbusSimulatorContext.build_registers_from_value(value, True) - for i in range(start, stop, 2): - regs = self.runtime.registers[i : i + 2] - if regs[0].type != CellType.INVALID or regs[1].type != CellType.INVALID: - raise RuntimeError(f'ERROR "{Label.type_uint32}" {i},{i + 1} used') - regs[0].value = regs_value[0] - regs[0].type = CellType.UINT32 - regs[0].action = action - regs[0].action_kwargs = action_kwargs - regs[1].value = regs_value[1] - regs[1].type = CellType.NEXT + regs_value = ModbusSimulatorContext.build_registers_from_value(value, True,4,False) + self.handle_type_4Bytes( start, stop, regs_value, action, action_kwargs,CellType.UINT32) + + def handle_type_uint64(self, start, stop, value, action, action_kwargs): + """Handle type uint64.""" + regs_value = ModbusSimulatorContext.build_registers_from_value(value, True,8,False) + self.handle_type_8Bytes( start, stop, regs_value, action, action_kwargs,CellType.UINT64) def handle_type_float32(self, start, stop, value, action, action_kwargs): """Handle type uint32.""" - regs_value = ModbusSimulatorContext.build_registers_from_value(value, False) - for i in range(start, stop, 2): - regs = self.runtime.registers[i : i + 2] - if regs[0].type != CellType.INVALID or regs[1].type != CellType.INVALID: - raise RuntimeError(f'ERROR "{Label.type_float32}" {i},{i + 1} used') - regs[0].value = regs_value[0] - regs[0].type = CellType.FLOAT32 - regs[0].action = action - regs[0].action_kwargs = action_kwargs - regs[1].value = regs_value[1] - regs[1].type = CellType.NEXT + regs_value = ModbusSimulatorContext.build_registers_from_value(value, False,4,None) + self.handle_type_4Bytes( start, stop, regs_value, action, action_kwargs,CellType.FLOAT32) + + def handle_type_float64(self, start, stop, value, action, action_kwargs): + """Handle type float64.""" + regs_value = ModbusSimulatorContext.build_registers_from_value(value, False,8,None) + self.handle_type_8Bytes( start, stop, regs_value, action, action_kwargs,CellType.FLOAT64) def handle_type_string(self, start, stop, value, action, action_kwargs): """Handle type string.""" @@ -208,7 +317,7 @@ def handle_type_string(self, start, stop, value, action, action_kwargs): for i in range(stop - start): reg = self.runtime.registers[start + i] if reg.type != CellType.INVALID: - raise RuntimeError(f'ERROR "{Label.type_string}" {start + i} used') + raise RuntimeError(f'ERROR "{Label.type_string}" at location {start + i} is already being used') j = i * 2 reg.value = int.from_bytes(bytes(value[j : j + 2], "UTF-8"), "big") reg.type = CellType.NEXT @@ -349,10 +458,17 @@ def setup(self, config, custom_actions) -> None: self.runtime.action_methods.append(method) i += 1 self.runtime.registerType_name_to_id = { - Label.type_bits: CellType.BITS, + Label.type_bitfield16: CellType.BITFIELD16, + Label.type_bitfield32: CellType.BITFIELD32, + Label.type_bitfield64: CellType.BITFIELD64, + Label.type_int16: CellType.INT16, + Label.type_int32: CellType.INT32, + Label.type_int64: CellType.INT64, Label.type_uint16: CellType.UINT16, Label.type_uint32: CellType.UINT32, + Label.type_uint64: CellType.UINT64, Label.type_float32: CellType.FLOAT32, + Label.type_float64: CellType.FLOAT64, Label.type_string: CellType.STRING, Label.next: CellType.NEXT, Label.invalid: CellType.INVALID, @@ -412,16 +528,29 @@ class ModbusSimulatorContext(ModbusBaseSlaveContext): "shared blocks": True, --> share memory for all blocks (largest size wins) "defaults": { "value": { --> Initial values (can be overwritten) - "bits": 0x01, + "bitfield16": 0x0001, -> e.g., a 16-bitmask + "bitfield32": 0x00010000,-> e.g., a 32-bitmask + "bitfield64": 0x0000000100000000,-> e.g., a 64-bitmask + "int16": -16, + "int32": -32, + "int64": -64, "uint16": 122, "uint32": 67000, + "uint64": 67000, "float32": 127.4, + "float64": 127.4, "string": " ", }, "action": { --> default action (can be overwritten) - "bits": None, + "bitfield16": None, + "bitfield32": None, + "bitfield64": None, + "int16": None, + "int32": None, + "int64": None, "uint16": None, "uint32": None, + "uint64": None, "float32": None, "string": None, }, @@ -435,21 +564,38 @@ class ModbusSimulatorContext(ModbusBaseSlaveContext): "write": [ --> allow write, efault is ReadOnly [5, 5] --> start, end bytes, repeated as needed ], - "bits": [ --> Define bits (1 register == 2 bytes) - [30, 31], --> start, end registers, repeated as needed - {"addr": [32, 34], "value": 0xF1}, --> with value - {"addr": [35, 36], "action": "increment"}, --> with action - {"addr": [37, 38], "action": "increment", "value": 0xF1} --> with action and value - {"addr": [37, 38], "action": "increment", "kwargs": {"min": 0, "max": 100}} --> with action with arguments + "bitfield16": [ --> Define bits (1 register == 2 bytes) + [30, 30], --> start, end registers, + {"addr": [32, 32], "value": 0xF1}, --> with value + {"addr": [35, 35], "action": "increment"}, --> with action + {"addr": [37, 37], "action": "increment", "value": 0xF1} --> with action and value + {"addr": [37, 37], "action": "increment", "kwargs": {"min": 0, "max": 100}} --> with action with arguments + ], + "int16": [ --> Define int16,signed (1 register == 2 bytes) + --> similar to bitfield16, but intended to represent a 16bit SIGNED number (-32768 <-> 32767) + ], + "int32": [ --> Define 32 bit signed integers (2 registers == 4 bytes) + --> similar to int16, but intended to represent a 32bit SIGNED number, occupies 2 registers + {'addr': [88, 89], 'value': 32} + ], + "int64": [ --> Define 64 bit signed integers (4 registers == 8 bytes) + --> similar to int32, uses 4 registers + {'addr': [88, 91], 'value': 64} ], "uint16": [ --> Define uint16 (1 register == 2 bytes) - --> same as type_bits + --> similar to int16, unsigned + ], + "uint32": [ --> Define 32 bit unsigned integers (2 registers == 4 bytes) + --> similar to int32, unsigned ], - "uint32": [ --> Define 32 bit integers (2 registers == 4 bytes) - --> same as type_bits + "uint64": [ --> Define 32 bit unsigned integers (4 registers == 8 bytes) + --> similar to int64, unsigned ], "float32": [ --> Define 32 bit floats (2 registers == 4 bytes) - --> same as type_bits + --> similar to int32, but encoding a floating number + ], + "float64": [ --> Define 64 bit floats (4 registers == 4 bytes) + --> similar to int32, but encoding a floating number ], "string": [ --> Define strings (variable number of registers (each 2 bytes)) [21, 22], --> start, end registers, define 1 string @@ -486,7 +632,7 @@ def __init__( # -------------------------------------------- # Simulator server interface # -------------------------------------------- - def get_text_register(self, register): + def get_text_register(self, register): # noqa: C901 """Get raw register.""" reg = self.registers[register] text_cell = TextCell() @@ -497,21 +643,48 @@ def get_text_register(self, register): text_cell.action = self.action_id_to_name[reg.action] if reg.action_kwargs: text_cell.action = f"{text_cell.action}({reg.action_kwargs})" - if reg.type in (CellType.INVALID, CellType.UINT16, CellType.NEXT): + if reg.type in (CellType.INVALID, CellType.NEXT): text_cell.value = str(reg.value) build_len = 0 - elif reg.type == CellType.BITS: - text_cell.value = hex(reg.value) + elif reg.type == CellType.BITFIELD16: + tmp_regs = [reg.value] + value=self.build_value_from_registers(tmp_regs, True,2,False) + text_cell.value ='0x' + hex(value)[2:].zfill(4) build_len = 0 - elif reg.type == CellType.UINT32: + elif reg.type == CellType.BITFIELD32: + tmp_regs = [reg.value, self.registers[register + 1].value] + value=self.build_value_from_registers(tmp_regs, True,4,False) + text_cell.value ='0x' + hex(value)[2:].zfill(8) + build_len = 2 + elif reg.type == CellType.BITFIELD64: + tmp_regs = [reg.value, self.registers[register + 1].value, self.registers[register + 2].value, self.registers[register + 3].value] + value=self.build_value_from_registers(tmp_regs, True,8,False) + text_cell.value ='0x' + hex(value)[2:].zfill(16) + build_len = 3 + elif reg.type in (CellType.UINT16,CellType.INT16): + tmp_regs = [reg.value, self.registers[register + 1].value] + is_signed= bool( reg.type ==CellType.INT16 ) + text_cell.value = str(self.build_value_from_registers(tmp_regs, True,2,is_signed)) + build_len = 1 + elif reg.type in (CellType.UINT32,CellType.INT32): tmp_regs = [reg.value, self.registers[register + 1].value] - text_cell.value = str(self.build_value_from_registers(tmp_regs, True)) + is_signed=bool( reg.type ==CellType.INT32 ) + text_cell.value = str(self.build_value_from_registers(tmp_regs, True,4,is_signed)) + build_len = 1 + elif reg.type in (CellType.UINT64,CellType.INT64): + tmp_regs = [reg.value, self.registers[register + 1].value,self.registers[register + 2].value, self.registers[register + 3].value] + is_signed=bool( reg.type ==CellType.INT64 ) + text_cell.value = str(self.build_value_from_registers(tmp_regs, True,8,is_signed)) build_len = 1 elif reg.type == CellType.FLOAT32: tmp_regs = [reg.value, self.registers[register + 1].value] - text_cell.value = str(self.build_value_from_registers(tmp_regs, False)) + text_cell.value = str(self.build_value_from_registers(tmp_regs, False,4,None)) build_len = 1 - else: # reg.type == CellType.STRING: + elif reg.type == CellType.FLOAT64: + tmp_regs = [reg.value, self.registers[register + 1].value, self.registers[register + 2].value, self.registers[register + 3].value] + text_cell.value = str(self.build_value_from_registers(tmp_regs, False,8,None)) + build_len = 3 + elif reg.type == CellType.STRING: j = register text_cell.value = "" while True: @@ -524,6 +697,10 @@ def get_text_register(self, register): if self.registers[j].type != CellType.NEXT: break build_len = j - register - 1 + else: + #Make sure all data types have a custom text generator, otherwise raise an error + raise RuntimeError('There is no text representation for the input type') + reg_txt = f"{register}-{register + build_len}" if build_len else f"{register}" return reg_txt, text_cell @@ -534,7 +711,7 @@ def get_text_register(self, register): _write_func_code = (5, 6, 15, 16, 22, 23) _bits_func_code = (1, 2, 5, 15) - def loop_validate(self, address, end_address, fx_write): + def loop_validate(self, address, end_address, fx_write): # noqa: C901 """Validate entry in loop. :meta private: @@ -549,17 +726,26 @@ def loop_validate(self, address, end_address, fx_write): continue if reg.type == CellType.NEXT: return False - if reg.type in (CellType.BITS, CellType.UINT16): + if reg.type in (CellType.BITFIELD16, CellType.UINT16,CellType.INT16): i += 1 - elif reg.type in (CellType.UINT32, CellType.FLOAT32): + elif reg.type in (CellType.BITFIELD32,CellType.UINT32,CellType.INT32, CellType.FLOAT32): if i + 1 >= end_address: return False i += 2 - else: + elif reg.type in (CellType.BITFIELD64,CellType.UINT64,CellType.INT64, CellType.FLOAT64): + if i + 3 >= end_address: + return False + i += 4 + elif reg.type == CellType.STRING: i += 1 while i < end_address: if self.registers[i].type == CellType.NEXT: i += 1 + else: + break + else: + raise RuntimeError('A cell without validation handler has been found') + return True def validate(self, func_code, address, count=1): @@ -653,58 +839,113 @@ def action_random(cls, registers, inx, cell, minval=1, maxval=65536): :meta private: """ - if cell.type in (CellType.BITS, CellType.UINT16): + if cell.type in (CellType.BITFIELD16, CellType.INT16, CellType.UINT16): + is_signed=bool( CellType.INT16) registers[inx].value = random.randint(int(minval), int(maxval)) + elif cell.type in (CellType.BITFIELD32, CellType.INT32, CellType.UINT32): + is_signed=bool( CellType.INT32) + regs = cls.build_registers_from_value( + random.randint(int(minval), int(maxval)),True,4,is_signed + ) + registers[inx].value = regs[0] + registers[inx + 1].value = regs[1] + elif cell.type in (CellType.BITFIELD64, CellType.INT64, CellType.UINT64): + is_signed=bool( CellType.INT64) + regs = cls.build_registers_from_value( + random.randint(int(minval), int(maxval)),True,8,is_signed + ) + registers[inx].value = regs[0] + registers[inx + 1].value = regs[1] + registers[inx + 2].value = regs[2] + registers[inx + 3].value = regs[3] elif cell.type == CellType.FLOAT32: regs = cls.build_registers_from_value( - random.uniform(float(minval), float(maxval)), False + random.uniform(float(minval), float(maxval)),False,4 ,None ) registers[inx].value = regs[0] registers[inx + 1].value = regs[1] - elif cell.type == CellType.UINT32: + elif cell.type == CellType.FLOAT64: regs = cls.build_registers_from_value( - random.randint(int(minval), int(maxval)), True + random.uniform(float(minval), float(maxval)),False,8, None ) registers[inx].value = regs[0] registers[inx + 1].value = regs[1] + registers[inx + 2].value = regs[2] + registers[inx + 3].value = regs[3] + @classmethod - def action_increment(cls, registers, inx, cell, minval=None, maxval=None): + def action_increment(cls, registers, inx, cell, minval=None, maxval=None): # noqa: C901 """Increment value reset with overflow. :meta private: """ reg = registers[inx] reg2 = registers[inx + 1] - if cell.type in (CellType.BITS, CellType.UINT16): - value = reg.value + 1 + reg3 = registers[inx + 2] + reg4 = registers[inx + 3] + + if cell.type in (CellType.BITFIELD16,CellType.INT16, CellType.UINT16): + is_signed=bool( CellType.INT16) + tmp_reg = [reg.value] + value = cls.build_value_from_registers(tmp_reg, True,2,is_signed) + value += 1 if maxval and value > maxval: value = minval if minval and value < minval: value = minval - reg.value = value + new_regs = cls.build_registers_from_value(value, True,2,is_signed) + reg.value = new_regs[0] + elif cell.type in (CellType.BITFIELD32,CellType.INT32, CellType.UINT32): + is_signed=bool( CellType.INT32) + tmp_reg = [reg.value, reg2.value] + value = cls.build_value_from_registers(tmp_reg, True,4,is_signed) + value += 1 + if maxval and value > maxval: + value = minval + if minval and value < minval: + value = minval + new_regs = cls.build_registers_from_value(value, True,4,is_signed) + reg.value = new_regs[0] + reg2.value = new_regs[1] + elif cell.type in (CellType.BITFIELD64,CellType.INT64, CellType.UINT64): + is_signed=bool( CellType.INT64) + tmp_reg = [reg.value, reg2.value, reg3.value, reg4.value] + value = cls.build_value_from_registers(tmp_reg, True,8,is_signed) + value += 1 + if maxval and value > maxval: + value = minval + if minval and value < minval: + value = minval + new_regs = cls.build_registers_from_value(value, True,8,is_signed) + reg.value = new_regs[0] + reg2.value = new_regs[1] + reg3.value = new_regs[2] + reg4.value = new_regs[3] elif cell.type == CellType.FLOAT32: tmp_reg = [reg.value, reg2.value] - value = cls.build_value_from_registers(tmp_reg, False) + value = cls.build_value_from_registers(tmp_reg, False,4,None) value += 1.0 if maxval and value > maxval: value = minval if minval and value < minval: value = minval - new_regs = cls.build_registers_from_value(value, False) + new_regs = cls.build_registers_from_value(value, False,4,None) reg.value = new_regs[0] reg2.value = new_regs[1] - elif cell.type == CellType.UINT32: - tmp_reg = [reg.value, reg2.value] - value = cls.build_value_from_registers(tmp_reg, True) - value += 1 + elif cell.type == CellType.FLOAT64: + tmp_reg = [reg.value, reg2.value, reg3.value, reg4.value] + value = cls.build_value_from_registers(tmp_reg, False,8,None) + value += 1.0 if maxval and value > maxval: value = minval if minval and value < minval: value = minval - new_regs = cls.build_registers_from_value(value, True) + new_regs = cls.build_registers_from_value(value, False,8,None) reg.value = new_regs[0] reg2.value = new_regs[1] + reg3.value = new_regs[2] + reg4.value = new_regs[3] @classmethod def action_timestamp(cls, registers, inx, _cell, **_kwargs): @@ -737,17 +978,24 @@ def action_uptime(cls, registers, inx, cell, **_kwargs): """ value = int(datetime.now().timestamp()) - cls.start_time + 1 - if cell.type in (CellType.BITS, CellType.UINT16): + if cell.type in (CellType.BITFIELD16, CellType.UINT16): registers[inx].value = value elif cell.type == CellType.FLOAT32: - regs = cls.build_registers_from_value(value, False) + regs = cls.build_registers_from_value(value, False,4,None) registers[inx].value = regs[0] registers[inx + 1].value = regs[1] elif cell.type == CellType.UINT32: - regs = cls.build_registers_from_value(value, True) + regs = cls.build_registers_from_value(value, True,4,False) + registers[inx].value = regs[0] + registers[inx + 1].value = regs[1] + elif cell.type == CellType.FLOAT64: + regs = cls.build_registers_from_value(value, False,8,None) + registers[inx].value = regs[0] + registers[inx + 1].value = regs[1] + elif cell.type == CellType.UINT64: + regs = cls.build_registers_from_value(value, True,8,False) registers[inx].value = regs[0] registers[inx + 1].value = regs[1] - # -------------------------------------------- # Internal helper methods # -------------------------------------------- @@ -760,16 +1008,18 @@ def validate_type(self, func_code, real_address, count) -> bool: check: tuple if func_code in self._bits_func_code: # Bit access - check = (CellType.BITS, -1) + check = (CellType.BITFIELD16, -1) reg_step = 1 elif count % 2: # 16 bit access - check = (CellType.UINT16, CellType.STRING) + check = (CellType.BITFIELD16,CellType.INT16,CellType.UINT16, CellType.STRING) reg_step = 1 - else: - check = (CellType.UINT32, CellType.FLOAT32, CellType.STRING) + elif count % 4: + check = (CellType.BITFIELD32,CellType.INT32,CellType.UINT32, CellType.FLOAT32, CellType.STRING) reg_step = 2 - + elif count % 8: + check = (CellType.BITFIELD64,CellType.INT64,CellType.UINT64, CellType.FLOAT64) + reg_step = 4 for i in range(real_address, real_address + count, reg_step): if self.registers[i].type in check: continue @@ -779,25 +1029,69 @@ def validate_type(self, func_code, real_address, count) -> bool: return True @classmethod - def build_registers_from_value(cls, value, is_int): + def build_registers_from_value(cls, value, is_int,n_bytes,is_signed): # noqa: C901 """Build registers from int32 or float32.""" - regs = [0, 0] if is_int: - value_bytes = int.to_bytes(value, 4, "big") + if n_bytes==2 and is_signed is False: + value_bytes = struct.pack(">H", value)#int.to_bytes(value, 4, "big") + if n_bytes==2 and is_signed is True: + value_bytes = struct.pack(">h", value)#int.to_bytes(value, 4, "big") + if n_bytes==4 and is_signed is False: + value_bytes = struct.pack(">I", value)#int.to_bytes(value, 4, "big") + if n_bytes==4 and is_signed is True: + value_bytes = struct.pack(">i", value)#int.to_bytes(value, 4, "big") + if n_bytes==8 and is_signed is False: + value_bytes = struct.pack(">Q", value)#int.to_bytes(value, 4, "big") + if n_bytes==8 and is_signed is True: + value_bytes = struct.pack(">q", value)#int.to_bytes(value, 4, "big") else: - value_bytes = struct.pack(">f", value) - regs[0] = int.from_bytes(value_bytes[:2], "big") - regs[1] = int.from_bytes(value_bytes[-2:], "big") + if n_bytes==4: + value_bytes = struct.pack(">f", value) + if n_bytes==8: + value_bytes = struct.pack(">d", value)#int.to_bytes(value, 4, "big") + + if n_bytes==2: + regs = [0] + regs[0] = int.from_bytes(value_bytes[:2], "big") + if n_bytes==4: + regs = [0, 0] + regs[0] = int.from_bytes(value_bytes[:2], "big") + regs[1] = int.from_bytes(value_bytes[-2:], "big") + if n_bytes==8: + regs = [0, 0,0,0] + regs[0] = int.from_bytes(value_bytes[:2], "big") + regs[1] = int.from_bytes(value_bytes[2:4], "big") + regs[2] = int.from_bytes(value_bytes[4:6], "big") + regs[3] = int.from_bytes(value_bytes[6:8], "big") + return regs @classmethod - def build_value_from_registers(cls, registers, is_int): - """Build int32 or float32 value from registers.""" - value_bytes = int.to_bytes(registers[0], 2, "big") + int.to_bytes( - registers[1], 2, "big" - ) + def build_value_from_registers(cls, registers, is_int,n_bytes,is_signed): # noqa: C901 + """Build int16,int32,int64 or float32 value from registers.""" + if n_bytes==2: + value_bytes = int.to_bytes(registers[0], 2, "big") + if n_bytes==4: + value_bytes = int.to_bytes(registers[0], 2, "big") + int.to_bytes(registers[1], 2, "big") + if n_bytes==8: + value_bytes = int.to_bytes(registers[0], 2, "big") + int.to_bytes(registers[1], 2, "big") + int.to_bytes( registers[2], 2, "big" )+ int.to_bytes(registers[3], 2, "big" ) + if is_int: - value = int.from_bytes(value_bytes, "big") + if n_bytes==2 and is_signed is False: + value = struct.unpack(">H", value_bytes) + if n_bytes==2 and is_signed is True: + value = struct.unpack(">h", value_bytes) + if n_bytes==4 and is_signed is False: + value = struct.unpack(">I", value_bytes) + if n_bytes==4 and is_signed is True: + value = struct.unpack(">i", value_bytes) + if n_bytes==8 and is_signed is False: + value = struct.unpack(">Q", value_bytes) + if n_bytes==8 and is_signed is True: + value = struct.unpack(">q", value_bytes) else: - value = struct.unpack(">f", value_bytes)[0] - return value + if n_bytes==4: + value = struct.unpack(">f", value_bytes) + if n_bytes==8: + value = struct.unpack(">d", value_bytes) + return value[0] diff --git a/pymodbus/server/simulator/setup.json b/pymodbus/server/simulator/setup.json index ea3213b62..9b83e1f92 100644 --- a/pymodbus/server/simulator/setup.json +++ b/pymodbus/server/simulator/setup.json @@ -78,17 +78,31 @@ "type exception": true, "defaults": { "value": { - "bits": 0, + "bitfield16": 0, + "bitfield32": 0, + "bitfield64": 0, + "int16": 0, + "int32": 0, + "int64": 0, "uint16": 0, "uint32": 0, + "uint64": 0, "float32": 0.0, + "float64": 0.0, "string": " " }, "action": { - "bits": null, + "bitfield16": null, + "bitfield32": null, + "bitfield64": null, + "int16": "increment", + "int32": "increment", + "int64": "increment", "uint16": "increment", "uint32": "increment", + "uint64": "increment", "float32": "increment", + "float64": "increment", "string": null } } @@ -99,21 +113,38 @@ "write": [ 3 ], - "bits": [ + "bitfield16": [ {"addr": 2, "value": 7} ], + "bitfield32": [ + {"addr": [28, 29], "value": 31, "action": null} + ], + "bitfield64": [ + {"addr": [30, 33], "value": 63, "action": null} + ], + "int16": [ + {"addr": [27, 27], "value": -17001, "action": "increment"} + ], + "int32": [ + {"addr": [25, 26], "value": -617001, "action": "increment"} + ], + "int64": [ + {"addr": [21, 24], "value": -64000000, "action": "increment"} + ], "uint16": [ - {"addr": 3, "value": 17001, "action": null}, + {"addr": 3, "value": 17001, "action": "random"}, 2100 ], "uint32": [ - {"addr": [4, 5], "value": 617001, "action": null}, + {"addr": [4, 5], "value": 617001, "action": "random"}, [3037, 3038] ], + "uint64":[{"addr": [12, 15], "value": 64000000, "action": "random"}], "float32": [ {"addr": [6, 7], "value": 404.17}, [4100, 4101] ], + "float64":[{"addr": [8, 11], "value": 64.64}], "string": [ 5047, {"addr": [16, 20], "value": "A_B_C_D_E_"} @@ -131,17 +162,31 @@ "type exception": true, "defaults": { "value": { - "bits": 0, + "bitfield16": 0, + "bitfield32": 0, + "bitfield64": 0, + "int16": 0, + "int32": 0, + "int64": 0, "uint16": 0, "uint32": 0, + "uint64": 0, "float32": 0.0, + "float64": 0.0, "string": " " }, "action": { - "bits": null, + "bitfield16": null, + "bitfield32": null, + "bitfield64": null, + "int16": null, + "int32": null, + "int64": null, "uint16": null, "uint32": null, + "uint64": null, "float32": null, + "float64": null, "string": null } } @@ -153,7 +198,7 @@ "write": [ 10 ], - "bits": [ + "bitfield16": [ 10, 1009, [1116, 1119], @@ -161,6 +206,16 @@ {"addr": [1148,1149], "value": 32117}, {"addr": [1208, 1306], "action": "random"} ], + "bitfield32": [ + ], + "bitfield64": [ + ], + "int16": [ + ], + "int32": [ + ], + "int64": [ + ], "uint16": [ 11, 2027, @@ -198,6 +253,7 @@ "kwargs": {"minval": 45000, "maxval": 55000} } ], + "uint64":[], "float32": [ [14, 15], [4047, 4048], @@ -217,6 +273,7 @@ "kwargs": {"minval": 45000.0, "maxval": 55000.0} } ], + "float64":[], "string": [ {"addr": [16, 20], "value": "A_B_C_D_E_"}, {"addr": [529, 544], "value": "Brand name, 32 bytes...........X"} @@ -225,4 +282,4 @@ ] } } -} +} \ No newline at end of file diff --git a/test/sub_server/test_simulator.py b/test/sub_server/test_simulator.py index aeefed8b7..59ddfd95b 100644 --- a/test/sub_server/test_simulator.py +++ b/test/sub_server/test_simulator.py @@ -28,22 +28,36 @@ class TestSimulator: "co size": 100, "di size": 150, "hr size": 200, - "ir size": 250, + "ir size": 300, "shared blocks": True, "type exception": False, "defaults": { "value": { - "bits": 0x0708, + "bitfield16": 0x0708, + "bitfield32": 0x10010708, + "bitfield64": 0x8001000000003708, + "int16": -1, + "int32": -45000, + "int64": -450000000, "uint16": 1, "uint32": 45000, + "uint64": 450000000, "float32": 127.4, + "float64": 10127.4, "string": "X", }, "action": { - "bits": None, + "bitfield16": None, + "bitfield32": None, + "bitfield64": None, + "int16": None, + "int32": None, + "int64": None, "uint16": None, "uint32": None, + "uint64": None, "float32": None, + "float64": None, "string": None, }, }, @@ -59,14 +73,40 @@ class TestSimulator: [21, 26], [33, 38], ], - "bits": [ + "bitfield16": [ 5, - [7, 8], + [7, 7], + [8, 8], {"addr": 10, "value": 0x81}, - {"addr": [11, 12], "value": 0x04342}, + {"addr": [11, 11], "value": 0x04342}, + {"addr": [12, 12], "value": 0x04342}, {"addr": 13, "action": "random"}, {"addr": 14, "value": 15, "action": "reset"}, ], + "bitfield32": [ + [50, 51], + {"addr": [52,53], "value": 0x04342}, + ], + "bitfield64": [ + [54, 57], + {"addr": [58,61], "value": 0x04342}, + ], + "int16": [ + 70, + [71, 71], + {"addr": 72, "value": 0x81}, + {"addr": [73, 73], "value": 0x04342}, + {"addr": 74, "action": "random"}, + {"addr": 75, "value": 15, "action": "reset"}, + ], + "int32": [ + [76, 77], + {"addr": [78,79], "value": 0x04342}, + ], + "int64": [ + [80, 83], + {"addr": [84,87], "value": 0x04342}, + ], "uint16": [ {"addr": 16, "value": 3124}, {"addr": [17, 18], "value": 5678}, @@ -88,16 +128,23 @@ class TestSimulator: "kwargs": {"minval": 10, "maxval": 80}, }, ], + "uint64": [ + {"addr": [62, 65], "value": 3124} + ], "float32": [ {"addr": [33, 34], "value": 3124.5}, {"addr": [35, 38], "value": 5678.19}, {"addr": [39, 42], "value": 345000.18, "action": "increment"}, ], + "float64": [ + {"addr": [66, 69], "value": 3124.5}, + ], "string": [ {"addr": [43, 44], "value": "Str"}, {"addr": [45, 48], "value": "Strxyz12"}, ], - "repeat": [{"addr": [0, 48], "to": [49, 147]}], + "repeat": [{"addr": [0, 95], "to": [96, 191]}, + {"addr": [0, 95], "to": [192, 287]}], } default_server_config = { @@ -124,16 +171,16 @@ class TestSimulator: Cell(), Cell(), Cell(), - Cell(type=CellType.BITS, access=True, value=0x0708), + Cell(type=CellType.BITFIELD16, access=True, value=0x0708), Cell(type=CellType.INVALID), - Cell(type=CellType.BITS, access=True, value=0x0708), - Cell(type=CellType.BITS, access=True, value=0x0708), + Cell(type=CellType.BITFIELD16, access=True, value=0x0708), + Cell(type=CellType.BITFIELD16, access=True, value=0x0708), Cell(type=CellType.INVALID), - Cell(type=CellType.BITS, value=0x81), # 10 - Cell(type=CellType.BITS, value=0x4342), - Cell(type=CellType.BITS, value=0x4342), - Cell(type=CellType.BITS, value=1800, action=2), - Cell(type=CellType.BITS, value=15, action=3), + Cell(type=CellType.BITFIELD16, value=0x81), # 10 + Cell(type=CellType.BITFIELD16, value=0x4342), + Cell(type=CellType.BITFIELD16, value=0x4342), + Cell(type=CellType.BITFIELD16, value=1800, action=2), + Cell(type=CellType.BITFIELD16, value=15, action=3), Cell(type=CellType.INVALID), Cell(type=CellType.UINT16, access=True, value=3124), Cell(type=CellType.UINT16, access=True, value=5678), @@ -194,20 +241,20 @@ def setup_method(self): def test_pack_unpack_values(self): """Test the pack unpack methods.""" value = 32145678 - regs = ModbusSimulatorContext.build_registers_from_value(value, True) - test_value = ModbusSimulatorContext.build_value_from_registers(regs, True) + regs = ModbusSimulatorContext.build_registers_from_value(value, True,4,False) + test_value = ModbusSimulatorContext.build_value_from_registers(regs, True,4,False) assert value == test_value value = 3.14159265358979 - regs = ModbusSimulatorContext.build_registers_from_value(value, False) - test_value = ModbusSimulatorContext.build_value_from_registers(regs, False) + regs = ModbusSimulatorContext.build_registers_from_value(value, False,4,None) + test_value = ModbusSimulatorContext.build_value_from_registers(regs, False,4,None) assert round(value, 6) == round(test_value, 6) def test_simulator_config_verify(self): """Test basic configuration.""" # Manually build expected memory image and then compare. - assert self.simulator.register_count == 250 - for offset in (0, 49, 98): + assert self.simulator.register_count == 300 + for offset in (0, 96, 192): for i, test_cell in enumerate(self.test_registers): reg = self.simulator.registers[i + offset] assert reg.type == test_cell.type, f"at index {i} - {offset}" @@ -229,14 +276,14 @@ def test_simulator_config_verify2(self): # Manually build expected memory image and then compare. exc_setup = copy.deepcopy(self.default_config) exc_setup[Label.setup][Label.shared_blocks] = False - exc_setup[Label.setup][Label.co_size] = 15 - exc_setup[Label.setup][Label.di_size] = 15 - exc_setup[Label.setup][Label.hr_size] = 15 - exc_setup[Label.setup][Label.ir_size] = 15 + exc_setup[Label.setup][Label.co_size] = 150 + exc_setup[Label.setup][Label.di_size] = 150 + exc_setup[Label.setup][Label.hr_size] = 150 + exc_setup[Label.setup][Label.ir_size] = 150 del exc_setup[Label.repeat] exc_setup[Label.repeat] = [] simulator = ModbusSimulatorContext(exc_setup, None) - assert simulator.register_count == 60 + assert simulator.register_count == 600 for i, test_cell in enumerate(self.test_registers): reg = simulator.registers[i] assert reg.type == test_cell.type, f"at index {i}" @@ -249,7 +296,7 @@ def test_simulator_invalid_config(self): with pytest.raises(RuntimeError): ModbusSimulatorContext(exc_setup, None) for entry in ( - (Label.type_bits, 5), + (Label.type_bitfield16, 5), (Label.type_uint16, 16), (Label.type_uint32, [31, 32]), (Label.type_float32, [33, 34]), @@ -260,7 +307,7 @@ def test_simulator_invalid_config(self): with pytest.raises(RuntimeError): ModbusSimulatorContext(exc_setup, None) exc_setup = copy.deepcopy(self.default_config) - del exc_setup[Label.type_bits] + del exc_setup[Label.type_bitfield16] with pytest.raises(RuntimeError): ModbusSimulatorContext(exc_setup, None) exc_setup = copy.deepcopy(self.default_config) @@ -269,7 +316,7 @@ def test_simulator_invalid_config(self): ModbusSimulatorContext(exc_setup, None) exc_setup = copy.deepcopy(self.default_config) exc_setup[Label.setup][Label.defaults][Label.action][ - Label.type_bits + Label.type_bitfield16 ] = "bad action" with pytest.raises(RuntimeError): ModbusSimulatorContext(exc_setup, None) @@ -286,7 +333,7 @@ def test_simulator_invalid_config(self): with pytest.raises(RuntimeError): ModbusSimulatorContext(exc_setup, None) exc_setup = copy.deepcopy(self.default_config) - exc_setup[Label.type_bits].append(700) + exc_setup[Label.type_bitfield16].append(700) with pytest.raises(RuntimeError): ModbusSimulatorContext(exc_setup, None) exc_setup = copy.deepcopy(self.default_config) @@ -297,7 +344,7 @@ def test_simulator_invalid_config(self): exc_setup[Label.type_uint16].append(0) ModbusSimulatorContext(exc_setup, None) exc_setup = copy.deepcopy(self.default_config) - exc_setup[Label.type_uint16].append(250) + exc_setup[Label.type_uint16].append(350) with pytest.raises(RuntimeError): ModbusSimulatorContext(exc_setup, None) @@ -414,7 +461,7 @@ def test_simulator_get_text(self): """Test get_text_register().""" for test_reg, test_entry, test_cell in ( (1, "1", Cell(type=Label.invalid, action="none", value="0")), - (5, "5", Cell(type=Label.type_bits, action="none", value="0x708")), + (5, "5", Cell(type=Label.type_bitfield16, action="none", value="0x0708")), ( 31, "31-32", @@ -492,8 +539,8 @@ def test_simulator_action_reset(self): @pytest.mark.parametrize( ("celltype", "minval", "maxval", "value", "expected"), [ - (CellType.BITS, 50, 75, 73, (74, 75, 50)), - (CellType.BITS, 50, 75, 45, (50, 51, 52)), + (CellType.BITFIELD16, 50, 75, 73, (74, 75, 50)), + (CellType.BITFIELD16, 50, 75, 45, (50, 51, 52)), (CellType.UINT16, 50, 15075, 15073, (15074, 15075, 50)), (CellType.UINT16, 50, 75, 45, (50, 51, 52)), (CellType.UINT32, 50, 63075, 63073, (63074, 63075, 50)), @@ -518,17 +565,22 @@ def test_simulator_action_increment( exc_simulator.registers[30].action_kwargs = kwargs exc_simulator.registers[31].type = CellType.NEXT - is_int = celltype != CellType.FLOAT32 - reg_count = 1 if celltype in (CellType.BITS, CellType.UINT16) else 2 + is_int = False if celltype in (CellType.FLOAT32,CellType.FLOAT64) else True + reg_count = 1 if celltype in (CellType.BITFIELD16, CellType.UINT16) else 2 + n_bytes=2 + if celltype in (CellType.BITFIELD32, CellType.INT32, CellType.UINT32, CellType.FLOAT32): + n_bytes=4 + if celltype in (CellType.BITFIELD64, CellType.INT64, CellType.UINT64, CellType.FLOAT64): + n_bytes=8 regs = ( [value, 0] if reg_count == 1 - else ModbusSimulatorContext.build_registers_from_value(value, is_int) + else ModbusSimulatorContext.build_registers_from_value(value, is_int,n_bytes,False) ) exc_simulator.registers[30].value = regs[0] exc_simulator.registers[31].value = regs[1] for expect_value in expected: - if celltype != CellType.BITS: + if celltype != CellType.BITFIELD16: regs = exc_simulator.getValues(FX_READ_REG, 30, reg_count) else: reg_bits = exc_simulator.getValues(FX_READ_BIT, 30 * 16, 16) @@ -538,14 +590,14 @@ def test_simulator_action_increment( assert expect_value == regs[0], f"type({celltype})" else: new_value = ModbusSimulatorContext.build_value_from_registers( - regs, is_int + regs, is_int,n_bytes,False ) assert expect_value == new_value, f"type({celltype})" @pytest.mark.parametrize( ("celltype", "minval", "maxval"), [ - (CellType.BITS, 50, 75), + (CellType.BITFIELD16, 50, 75), (CellType.UINT16, 50, 15075), (CellType.UINT32, 50, 63075), (CellType.FLOAT32, 27.0, 16100.5), @@ -565,10 +617,15 @@ def test_simulator_action_random(self, celltype, minval, maxval): exc_simulator.registers[30].action = action exc_simulator.registers[30].action_kwargs = kwargs exc_simulator.registers[31].type = CellType.NEXT - is_int = celltype != CellType.FLOAT32 - reg_count = 1 if celltype in (CellType.BITS, CellType.UINT16) else 2 + is_int = False if celltype in (CellType.FLOAT32,CellType.FLOAT64) else True + n_bytes=2 + if celltype in (CellType.BITFIELD32, CellType.INT32, CellType.UINT32, CellType.FLOAT32): + n_bytes=4 + if celltype in (CellType.BITFIELD64, CellType.INT64, CellType.UINT64, CellType.FLOAT64): + n_bytes=8 + reg_count = 1 if celltype in (CellType.BITFIELD16, CellType.UINT16) else 2 for _i in range(100): - if celltype != CellType.BITS: + if celltype != CellType.BITFIELD16: regs = exc_simulator.getValues(FX_READ_REG, 30, reg_count) else: reg_bits = exc_simulator.getValues(FX_READ_BIT, 30 * 16, 16) @@ -578,7 +635,7 @@ def test_simulator_action_random(self, celltype, minval, maxval): new_value = regs[0] else: new_value = ModbusSimulatorContext.build_value_from_registers( - regs, is_int + regs, is_int,n_bytes,False ) assert minval <= new_value <= maxval @@ -599,3 +656,4 @@ async def test_simulator_server_tcp(self, unused_tcp_port): await task.run_forever(only_start=True) await asyncio.sleep(0.5) await task.stop() +