diff --git a/examples/datastore_simulator_share.py b/examples/datastore_simulator_share.py index c9ec7367d..953af2f69 100755 --- a/examples/datastore_simulator_share.py +++ b/examples/datastore_simulator_share.py @@ -54,6 +54,7 @@ "uint16": 1, "uint32": 45000, "float32": 127.4, + "float64": 505.14, "string": "X", }, "action": { @@ -61,6 +62,7 @@ "uint16": None, "uint32": None, "float32": None, + "float64": None, "string": None, }, }, @@ -102,11 +104,14 @@ {"addr": [33, 36], "value": 5678.19}, {"addr": [37, 40], "value": 345000.18, "action": "increment"}, ], + "float64": [ + {"addr": [41, 44], "value": -42478.53, "action": "random"}, + ], "string": [ - {"addr": [41, 42], "value": "Str"}, - {"addr": [43, 44], "value": "Strx"}, + {"addr": [45, 46], "value": "Str"}, + {"addr": [47, 48], "value": "Strx"}, ], - "repeat": [{"addr": [0, 45], "to": [46, 138]}], + "repeat": [{"addr": [0, 48], "to": [49, 147]}], } diff --git a/examples/simulator.py b/examples/simulator.py index 0797dcf3b..5109363bf 100755 --- a/examples/simulator.py +++ b/examples/simulator.py @@ -13,7 +13,7 @@ from pymodbus import FramerType from pymodbus.client import AsyncModbusTcpClient -from pymodbus.datastore import ModbusSimulatorContext +from pymodbus.datastore import CellType, ModbusSimulatorContext from pymodbus.server import ModbusSimulatorServer, get_simulator_commandline @@ -21,7 +21,7 @@ async def read_registers( - client, addr, count, is_int, curval=None, minval=None, maxval=None + client, addr, count, celltype: CellType, curval=None, minval=None, maxval=None ): """Run modbus call.""" rr = await client.read_holding_registers(addr, count=count, device_id=1) @@ -29,9 +29,11 @@ async def read_registers( if count == 1: value = rr.registers[0] else: - value = ModbusSimulatorContext.build_value_from_registers(rr.registers, is_int) - if not is_int: - value = round(value, 1) + value = ModbusSimulatorContext.build_value_from_registers( + rr.registers, celltype + ) + if not CellType.is_int(celltype): + value = round(value, 2) if curval: assert value == curval, f"{value} == {curval}" else: @@ -43,19 +45,30 @@ async def run_calls(client, count): _logger.info("### Read fixed/increment/random value of different types.") _logger.info("--> UINT16") for count in range(1, 5): - await read_registers(client, 1148, 1, True, curval=32117) - await read_registers(client, 2305, 1, True, curval=50 + count) - await read_registers(client, 2306, 1, True, minval=45, maxval=55) + await read_registers(client, 1148, 1, CellType.UINT16, curval=32117) + await read_registers(client, 2305, 1, CellType.UINT16, curval=50 + count) + await read_registers(client, 2306, 1, CellType.UINT16, minval=45, maxval=55) _logger.info("--> UINT32") - await read_registers(client, 3188, 2, True, curval=32514) - await read_registers(client, 3876, 2, True, curval=50000 + count) - await read_registers(client, 3878, 2, True, minval=45000, maxval=55000) + await read_registers(client, 3188, 2, CellType.UINT32, curval=32514) + await read_registers(client, 3876, 2, CellType.UINT32, curval=50000 + count) + await read_registers( + client, 3878, 2, CellType.UINT32, minval=45000, maxval=55000 + ) _logger.info("--> FLOAT32") - await read_registers(client, 4188, 2, False, curval=32514.2) - await read_registers(client, 4876, 2, False, curval=50000.0 + count) - await read_registers(client, 4878, 2, False, minval=45000.0, maxval=55000.0) + await read_registers(client, 4188, 2, CellType.FLOAT32, curval=32514.2) + await read_registers(client, 4876, 2, CellType.FLOAT32, curval=50000.0 + count) + await read_registers( + client, 4878, 2, CellType.FLOAT32, minval=45000.0, maxval=55000.0 + ) + + _logger.info("--> FLOAT64") + await read_registers(client, 5092, 4, CellType.FLOAT64, curval=-32514.2) + await read_registers(client, 5164, 4, CellType.FLOAT64, curval=-42.15 + count) + await read_registers( + client, 5244, 4, CellType.FLOAT64, minval=-4242, maxval=314.92 + ) async def run_simulator(): diff --git a/pymodbus/datastore/__init__.py b/pymodbus/datastore/__init__.py index b8f028255..ce2f0a5d2 100644 --- a/pymodbus/datastore/__init__.py +++ b/pymodbus/datastore/__init__.py @@ -1,6 +1,7 @@ """Datastore.""" __all__ = [ + "CellType", "ModbusDeviceContext", "ModbusSequentialDataBlock", "ModbusServerContext", @@ -13,5 +14,5 @@ ModbusServerContext, ) from .sequential import ModbusSequentialDataBlock -from .simulator import ModbusSimulatorContext +from .simulator import CellType, ModbusSimulatorContext from .sparse import ModbusSparseDataBlock diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py index 122d9833b..13785612b 100644 --- a/pymodbus/datastore/simulator.py +++ b/pymodbus/datastore/simulator.py @@ -7,6 +7,7 @@ import struct from collections.abc import Callable from datetime import datetime +from enum import IntEnum from typing import Any from ..constants import ExcCodes @@ -16,24 +17,48 @@ WORD_SIZE = 16 -@dataclasses.dataclass(frozen=True) -class CellType: +class CellType(IntEnum): """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 + INVALID = 0 + BITS = 1 + UINT16 = 2 + UINT32 = 3 + FLOAT32 = 4 + FLOAT64 = 5 + STRING = 6 + NEXT = 7 + + @classmethod + def register_count(cls, celltype: CellType) -> int: + """Return register length (a register is a world, 2 bytes value) for the given cell type.""" + if celltype in [cls.STRING, cls.INVALID, cls.NEXT]: + raise RuntimeError( + f"Invalid call to register_count with type {CellType(celltype).name} ({celltype})." + ) + + if celltype in [cls.BITS, cls.UINT16]: + return 1 + if cls.is_64(celltype): + return 4 + return 2 + + @classmethod + def is_int(cls, celltype: CellType) -> bool: + """Return True if the given cell type represents integer value.""" + return celltype in [cls.BITS, cls.UINT16, cls.UINT32] + + @classmethod + def is_64(cls, celltype: CellType) -> bool: + """Return True if the given cell type represents 64=bit (8 bytes, 4 registers) value.""" + return celltype in [cls.FLOAT64] @dataclasses.dataclass(repr=False, eq=False) class Cell: """Handle a single cell.""" - type: int = CellType.INVALID + type: CellType = CellType.INVALID access: bool = False value: int = 0 action: int = 0 @@ -88,6 +113,7 @@ class Label: # pylint: disable=too-many-instance-attributes type_uint16: str = "uint16" type_uint32: str = "uint32" type_float32: str = "float32" + type_float64: str = "float64" type_string: str = "string" uptime: str = "uptime" value: str = "value" @@ -141,6 +167,13 @@ def __init__(self, runtime: Any) -> None: 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, @@ -172,7 +205,9 @@ def handle_type_uint16(self, start, stop, value, action, action_parameters): def handle_type_uint32(self, start, stop, value, action, action_parameters): """Handle type uint32.""" - regs_value = ModbusSimulatorContext.build_registers_from_value(value, True) + regs_value = ModbusSimulatorContext.build_registers_from_value( + value, CellType.UINT32 + ) 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: @@ -185,8 +220,10 @@ def handle_type_uint32(self, start, stop, value, action, action_parameters): regs[1].type = CellType.NEXT def handle_type_float32(self, start, stop, value, action, action_parameters): - """Handle type uint32.""" - regs_value = ModbusSimulatorContext.build_registers_from_value(value, False) + """Handle type float32.""" + regs_value = ModbusSimulatorContext.build_registers_from_value( + value, CellType.FLOAT32 + ) 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: @@ -198,6 +235,30 @@ def handle_type_float32(self, start, stop, value, action, action_parameters): regs[1].value = regs_value[1] regs[1].type = CellType.NEXT + def handle_type_float64(self, start, stop, value, action, action_parameters): + """Handle type float64.""" + regs_value = ModbusSimulatorContext.build_registers_from_value( + value, CellType.FLOAT64 + ) + 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 "{Label.type_float64}" {i},{i + 1},{i + 2},{i + 3} used' + ) + regs[0].value = regs_value[0] + regs[0].type = CellType.FLOAT64 + regs[0].action = action + regs[0].action_parameters = action_parameters + for i in range(1, 4): + regs[i].value = regs_value[i] + regs[i].type = CellType.NEXT + def handle_type_string(self, start, stop, value, action, action_parameters): """Handle type string.""" regs = stop - start @@ -365,8 +426,9 @@ def setup(self, config, custom_actions) -> None: "uint16", # 2 "uint32", # 3 "float32", # 4 - "string", # 5 - "next", # 6 + "float64", # 5 + "string", # 6 + "next", # 7 ] self.config = config @@ -510,21 +572,13 @@ def get_text_register(self, register): text_cell.action = self.action_id_to_name[reg.action] if reg.action_parameters: text_cell.action = f"{text_cell.action}({reg.action_parameters})" - if reg.type in (CellType.INVALID, CellType.UINT16, CellType.NEXT): + if reg.type in [CellType.INVALID, CellType.UINT16, CellType.NEXT]: text_cell.value = str(reg.value) build_len = 0 elif reg.type == CellType.BITS: text_cell.value = hex(reg.value) build_len = 0 - elif reg.type == CellType.UINT32: - tmp_regs = [reg.value, self.registers[register + 1].value] - text_cell.value = str(self.build_value_from_registers(tmp_regs, True)) - 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)) - build_len = 1 - else: # reg.type == CellType.STRING: + elif reg.type == CellType.STRING: j = register text_cell.value = "" while True: @@ -536,8 +590,18 @@ def get_text_register(self, register): j += 1 if self.registers[j].type != CellType.NEXT: break - build_len = j - register - 1 - reg_txt = f"{register}-{register + build_len}" if build_len else f"{register}" + build_len = j - register + else: + build_len = CellType.register_count(reg.type) + text_cell.value = str( + self.build_value_from_registers( + self.registers[register : register + build_len], reg.type + ) + ) + + reg_txt = ( + f"{register}-{register + build_len - 1}" if build_len else f"{register}" + ) return reg_txt, text_cell # -------------------------------------------- @@ -555,26 +619,31 @@ def loop_validate(self, address, end_address, fx_write): i = address while i < end_address: reg = self.registers[i] + if (fx_write and not reg.access) or reg.type == CellType.INVALID: return False + if not self.type_exception: i += 1 continue + if reg.type == CellType.NEXT: return False - if reg.type in (CellType.BITS, CellType.UINT16): - i += 1 - elif reg.type in (CellType.UINT32, CellType.FLOAT32): - if i + 1 >= end_address: - return False - i += 2 - else: - i += 1 - while i < end_address: - if self.registers[i].type == CellType.NEXT: - i += 1 - else: - return False + + # Handle registers with unknown length + if reg.type in (CellType.STRING, CellType.NEXT): + return all( + self.registers[j].type == CellType.NEXT + for j in range(i + 1, end_address) + ) + step = CellType.register_count(reg.type) + + # Perform a single bounds check for multi-register types + if i + step - 1 >= end_address: + return False + + i += step + return True def validate(self, func_code, address, count=1): @@ -584,8 +653,8 @@ def validate(self, func_code, address, count=1): """ if func_code in self._bits_func_code: # Bit count, correct to register count - count = int((count + WORD_SIZE - 1) / WORD_SIZE) - address = int(address / 16) + count = (count + WORD_SIZE - 1) // WORD_SIZE + address = address // 16 real_address = self.fc_offset[func_code] + address if real_address < 0 or real_address > self.register_count: @@ -610,23 +679,20 @@ async def async_OLD_getValues( reg = self.registers[i] parameters = reg.action_parameters if reg.action_parameters else {} if reg.action: - self.action_methods[reg.action]( - self.registers, i, reg, **parameters - ) + self.action_methods[reg.action](self.registers, i, **parameters) + self.registers[i].count_read += 1 result.append(reg.value) else: # bit access - real_address = self.fc_offset[func_code] + int(address / 16) + real_address = self.fc_offset[func_code] + address // 16 bit_index = address % 16 - reg_count = int((count + bit_index + 15) / 16) + reg_count = (count + bit_index + 15) // 16 for i in range(real_address, real_address + reg_count): reg = self.registers[i] if reg.action: parameters = reg.action_parameters or {} - self.action_methods[reg.action]( - self.registers, i, reg, **parameters - ) + self.action_methods[reg.action](self.registers, i, **parameters) self.registers[i].count_read += 1 while count and bit_index < 16: result.append(bool(reg.value & (2**bit_index))) @@ -675,66 +741,59 @@ async def async_OLD_setValues(self, func_code, address, values) -> ExcCodes | No # -------------------------------------------- @classmethod - def action_random(cls, registers, inx, cell, minval=1, maxval=65536): + def action_random( + cls, + registers: list[Cell], + inx: int, + minval: int | float = 1, + maxval: int | float = 65536, + ) -> None: """Update with random value. :meta private: """ - if cell.type in (CellType.BITS, CellType.UINT16): - registers[inx].value = random.randint(int(minval), int(maxval)) - elif cell.type == CellType.FLOAT32: - regs = cls.build_registers_from_value( - random.uniform(float(minval), float(maxval)), False + new_values: list[int] = [] + celltype = registers[inx].type + if CellType.is_int(celltype): + new_values = cls.build_registers_from_value( + random.randint(int(minval), int(maxval)), celltype ) - registers[inx].value = regs[0] - registers[inx + 1].value = regs[1] - elif cell.type == CellType.UINT32: - regs = cls.build_registers_from_value( - random.randint(int(minval), int(maxval)), True + else: + new_values = cls.build_registers_from_value( + random.uniform(float(minval), float(maxval)), celltype ) - registers[inx].value = regs[0] - registers[inx + 1].value = regs[1] + + for i, value in enumerate(new_values): + registers[inx + i].value = value @classmethod - def action_increment(cls, registers, inx, cell, minval=None, maxval=None): + def action_increment( + cls, + registers: list[Cell], + inx: int, + minval: int | float | None = None, + maxval: int | float | None = None, + ) -> None: """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 - if maxval and value > maxval: - value = minval - if minval and value < minval: - value = minval - reg.value = value - elif cell.type == CellType.FLOAT32: - tmp_reg = [reg.value, reg2.value] - value = cls.build_value_from_registers(tmp_reg, False) - 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) - reg.value = new_regs[0] - reg2.value = new_regs[1] - else: # if cell.type == CellType.UINT32: - tmp_reg = [reg.value, reg2.value] - value = cls.build_value_from_registers(tmp_reg, True) - 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) - reg.value = new_regs[0] - reg2.value = new_regs[1] + celltype = registers[inx].type + value = cls.build_value_from_registers( + registers[inx : inx + CellType.register_count(celltype)], celltype + ) + value += 1 + if maxval is not None and value > maxval and minval is not None: + value = minval + if minval is not None and value < minval: + value = minval + + new_values = cls.build_registers_from_value(value, celltype) + for i, value in enumerate(new_values): + registers[inx + i].value = value @classmethod - def action_timestamp(cls, registers, inx, _cell, **_parameters): + def action_timestamp(cls, registers, inx, **_parameters): """Set current time. :meta private: @@ -749,7 +808,7 @@ def action_timestamp(cls, registers, inx, _cell, **_parameters): registers[inx + 6].value = system_time.second @classmethod - def action_reset(cls, _registers, _inx, _cell, **_parameters): + def action_reset(cls, _registers, _inx, **_parameters): """Reboot server. :meta private: @@ -757,48 +816,57 @@ def action_reset(cls, _registers, _inx, _cell, **_parameters): raise RuntimeError("RESET server") @classmethod - def action_uptime(cls, registers, inx, cell, **_parameters): + def action_uptime(cls, registers, inx, **_parameters): """Return uptime in seconds. :meta private: """ value = int(datetime.now().timestamp()) - cls.start_time + 1 - if cell.type in (CellType.BITS, CellType.UINT16): - registers[inx].value = value - elif cell.type == CellType.FLOAT32: - regs = cls.build_registers_from_value(value, False) - registers[inx].value = regs[0] - registers[inx + 1].value = regs[1] - else: # if cell.type == CellType.UINT32: - regs = cls.build_registers_from_value(value, True) - registers[inx].value = regs[0] - registers[inx + 1].value = regs[1] + new_values = cls.build_registers_from_value(value, registers[inx].type) + for i, value in enumerate(new_values): + registers[inx + i].value = value # -------------------------------------------- # Internal helper methods # -------------------------------------------- @classmethod - def build_registers_from_value(cls, value, is_int): - """Build registers from int32 or float32.""" - regs = [0, 0] - if is_int: - value_bytes = int.to_bytes(value, 4, "big") + def build_registers_from_value( + cls, value: int | float, celltype: CellType + ) -> list[int]: + """Build registers from int32, float32 or float64.""" + reg_count = CellType.register_count(celltype) + regs = [0] * reg_count + + if CellType.is_int(celltype): + value_bytes = int.to_bytes(int(value), reg_count * 2, "big") + elif CellType.is_64(celltype): + value_bytes = struct.pack(">d", value) 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") + + for i in range(reg_count): + regs[i] = int.from_bytes(value_bytes[i * 2 : (i * 2) + 2], "big") return regs @classmethod - def build_value_from_registers(cls, registers, is_int): + def build_value_from_registers( + cls, registers: list[Cell] | list[int], celltype: CellType + ) -> int | float: """Build int32 or float32 value from registers.""" - value_bytes = int.to_bytes(registers[0], 2, "big") + int.to_bytes( - registers[1], 2, "big" - ) - if is_int: + value_bytes: bytes = b"" + for i in range(CellType.register_count(celltype)): + reg = registers[i] + if isinstance(reg, Cell): + value_bytes += int.to_bytes(reg.value, 2, "big") + else: + value_bytes += int.to_bytes(reg, 2, "big") + + if CellType.is_int(celltype): value = int.from_bytes(value_bytes, "big") + elif CellType.is_64(celltype): + value = struct.unpack(">d", value_bytes)[0] else: value = struct.unpack(">f", value_bytes)[0] return value diff --git a/pymodbus/server/simulator/setup.json b/pymodbus/server/simulator/setup.json index a9f888568..53a6ac7d7 100644 --- a/pymodbus/server/simulator/setup.json +++ b/pymodbus/server/simulator/setup.json @@ -82,6 +82,7 @@ "uint16": 0, "uint32": 0, "float32": 0.0, + "float64": 0.0, "string": " " }, "action": { @@ -89,6 +90,7 @@ "uint16": "increment", "uint32": "increment", "float32": "increment", + "float64": "increment", "string": null } } @@ -114,6 +116,10 @@ {"addr": [6, 7], "value": 404.17}, [4100, 4101] ], + "float64": [ + {"addr": [8, 11], "value": -505.78}, + [4201, 4204] + ], "string": [ 5047, {"addr": [16, 20], "value": "A_B_C_D_E_"} @@ -135,6 +141,7 @@ "uint16": 0, "uint32": 0, "float32": 0.0, + "float64": 0.0, "string": " " }, "action": { @@ -142,6 +149,7 @@ "uint16": null, "uint32": null, "float32": null, + "float64": null, "string": null } } @@ -184,7 +192,7 @@ [3037, 3038], [3136, 3139], {"addr": [3174, 3175], "value": 1}, - {"addr": [3188,3189], "value": 32514}, + {"addr": [3188, 3189], "value": 32514}, {"addr": [3308, 3407], "action": null}, {"addr": [3688, 3875], "value": 115, "action": "increment"}, {"addr": [3876, 3877], @@ -211,14 +219,30 @@ "action": "increment", "parameters": {"minval": 45000.0, "maxval": 55000.0} }, - {"addr": [4878, 48779], + {"addr": [4878, 4879], "value": 50000.0, "action": "random", "parameters": {"minval": 45000.0, "maxval": 55000.0} } ], + "float64": [ + [16, 19], + [5049, 5052], + {"addr": [5092, 5095], "value": -32514.2}, + {"addr": [5160, 5239], "value": -42.15, "action": "increment"}, + {"addr": [5240, 5243], + "value": -42.15, + "action": "increment", + "parameters": {"minval": -100, "maxval": 142} + }, + {"addr": [5244, 5247], + "value": 48.24, + "action": "random", + "parameters": {"minval": -4242, "maxval": 314.92} + } + ], "string": [ - {"addr": [16, 20], "value": "A_B_C_D_E_"}, + {"addr": [20, 24], "value": "A_B_C_D_E_"}, {"addr": [529, 544], "value": "Brand name, 32 bytes...........X"} ], "repeat": [ diff --git a/pyproject.toml b/pyproject.toml index f30253308..3bfed94e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ development = [ "pytest-xdist>=3.6.1", "pytest-aiohttp>=1.1.1", "ruff>=0.15.20", + "pyserial>=3.5", "twine>=6.2.0", "types-Pygments", "types-pyserial", diff --git a/test/datastore/test_simulator_datastore.py b/test/datastore/test_simulator_datastore.py index ffbab9fcc..4b6deaab2 100644 --- a/test/datastore/test_simulator_datastore.py +++ b/test/datastore/test_simulator_datastore.py @@ -6,8 +6,8 @@ import pytest from pymodbus.constants import ExcCodes -from pymodbus.datastore import ModbusSimulatorContext -from pymodbus.datastore.simulator import Cell, CellType, Label +from pymodbus.datastore import CellType, ModbusSimulatorContext +from pymodbus.datastore.simulator import Cell, Label FX_READ_BIT = 1 @@ -33,6 +33,7 @@ class TestDatastoreSimulator: "uint16": 1, "uint32": 45000, "float32": 127.4, + "float64": -42.42, "string": "X", }, "action": { @@ -40,6 +41,7 @@ class TestDatastoreSimulator: "uint16": None, "uint32": None, "float32": None, + "float64": None, "string": None, }, }, @@ -54,6 +56,7 @@ class TestDatastoreSimulator: [16, 18], [21, 26], [33, 38], + [43, 46], ], "bits": [ 5, @@ -89,11 +92,15 @@ class TestDatastoreSimulator: {"addr": [35, 38], "value": 5678.19}, {"addr": [39, 42], "value": 345000.18, "action": "increment"}, ], + "float64": [ + {"addr": [43, 46], "value": 4242.487}, + {"addr": [47, 50], "value": -1237.879, "action": "increment"}, + ], "string": [ - {"addr": [43, 44], "value": "Str"}, - {"addr": [45, 48], "value": "Strxyz12"}, + {"addr": [51, 52], "value": "Str"}, + {"addr": [53, 56], "value": "Strxyz12"}, ], - "repeat": [{"addr": [0, 48], "to": [49, 147]}], + "repeat": [{"addr": [0, 56], "to": [57, 171]}], } default_server = { @@ -162,6 +169,14 @@ class TestDatastoreSimulator: Cell(type=CellType.NEXT, value=29958), # 40 Cell(type=CellType.FLOAT32, value=18600, action=1), Cell(type=CellType.NEXT, value=29958), + Cell(type=CellType.FLOAT64, access=True, value=16560), + Cell(type=CellType.NEXT, access=True, value=37500), + Cell(type=CellType.NEXT, access=True, value=44040), + Cell(type=CellType.NEXT, access=True, value=12583), + Cell(type=CellType.FLOAT64, value=49299, action=1), + Cell(type=CellType.NEXT, value=22404), + Cell(type=CellType.NEXT, value=6291), + Cell(type=CellType.NEXT, value=29884), # 50 Cell(type=CellType.STRING, value=int.from_bytes(bytes("St", "utf-8"), "big")), Cell(type=CellType.NEXT, value=int.from_bytes(bytes("r ", "utf-8"), "big")), Cell(type=CellType.STRING, value=int.from_bytes(bytes("St", "utf-8"), "big")), @@ -172,11 +187,11 @@ class TestDatastoreSimulator: ] @classmethod - def custom_action1(cls, _inx, _cell): + def custom_action1(cls, _registers, _inx): """Test action.""" @classmethod - def custom_action2(cls, _inx, _cell): + def custom_action2(cls, _registers, _inx): """Test action.""" custom_actions = { @@ -202,20 +217,35 @@ def test_simulator_datastore(self, device): 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, CellType.UINT32) + test_value = ModbusSimulatorContext.build_value_from_registers( + regs, CellType.UINT32 + ) 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, CellType.FLOAT32 + ) + test_value = ModbusSimulatorContext.build_value_from_registers( + regs, CellType.FLOAT32 + ) + assert round(value, 6) == round(test_value, 6) + + value = 2.718281828459045 + regs = ModbusSimulatorContext.build_registers_from_value( + value, CellType.FLOAT64 + ) + test_value = ModbusSimulatorContext.build_value_from_registers( + regs, CellType.FLOAT64 + ) assert round(value, 6) == round(test_value, 6) def test_simulator_config_verify(self, simulator): """Test basic configuration.""" # Manually build expected memory image and then compare. assert simulator.register_count == 250 - for offset in (0, 49, 98): + for offset in (0, 57, 114): for i, test_cell in enumerate(self.test_registers): reg = simulator.registers[i + offset] assert reg.type == test_cell.type, f"at index {i} - {offset}" @@ -262,7 +292,8 @@ def test_simulator_invalid_config1(self, device): (Label.type_uint16, 16), (Label.type_uint32, [31, 32]), (Label.type_float32, [33, 34]), - (Label.type_string, [43, 44]), + (Label.type_float64, [47, 50]), + (Label.type_string, [51, 52]), ], ) def test_simulator_invalid_config2(self, entry, device): @@ -388,7 +419,12 @@ def test_simulator_get_text(self, simulator): ), ), (33, "33-34", Cell(type=Label.type_float32, action="none", value="3124.5")), - (43, "43-44", Cell(type=Label.type_string, action="none", value="Str ")), + ( + 47, + "47-50", + Cell(type=Label.type_float64, action="increment", value="-1237.879"), + ), + (51, "51-52", Cell(type=Label.type_string, action="none", value="Str ")), ): reg = simulator.registers[test_reg] entry, cell = simulator.get_text_register(test_reg) @@ -460,6 +496,8 @@ async def test_simulator_action_reset(self, device): (CellType.UINT32, 50, 75, 45, (50, 51, 52)), (CellType.FLOAT32, 27.0, 16100.5, 16098.0, (16099.0, 16100.0, 27.0)), (CellType.FLOAT32, 27.0, 75.5, 24.0, (27.0, 28.0, 29.0)), + (CellType.FLOAT64, 27.0, 1615, 24.0, (27.0, 28.0, 29.0)), + (CellType.FLOAT64, -29.5, 75.5, -40, (-29.5, -28.5, -27.5)), ], ) async def test_simulator_action_increment( @@ -472,38 +510,35 @@ async def test_simulator_action_increment( "minval": minval, "maxval": maxval, } - exc_simulator.registers[30].type = celltype + exc_simulator.registers[30].type = celltype.value exc_simulator.registers[30].action = action exc_simulator.registers[30].action_parameters = parameters - exc_simulator.registers[31].type = CellType.NEXT + for i in range(3): + exc_simulator.registers[31 + i].type = CellType.NEXT + exc_simulator.registers[31 + i].action = 0 + + regs = ModbusSimulatorContext.build_registers_from_value(value, celltype) + reg_count = CellType.register_count(celltype) + + for i, new_value in enumerate(regs): + exc_simulator.registers[30 + i].value = new_value - is_int = celltype != CellType.FLOAT32 - reg_count = 1 if celltype in (CellType.BITS, CellType.UINT16) else 2 - regs = ( - [value, 0] - if reg_count == 1 - else ModbusSimulatorContext.build_registers_from_value(value, is_int) - ) - exc_simulator.registers[30].value = regs[0] - exc_simulator.registers[31].value = regs[1] for expect_value in expected: if celltype != CellType.BITS: regs = await exc_simulator.async_OLD_getValues( FX_READ_REG, 30, reg_count ) + reg_value = ModbusSimulatorContext.build_value_from_registers( + regs, celltype + ) + else: reg_bits = await exc_simulator.async_OLD_getValues( FX_READ_BIT, 30 * 16, 16 ) reg_value = sum(bit * 2**i for i, bit in enumerate(reg_bits)) - regs = [reg_value] - if reg_count == 1: - assert expect_value == regs[0], f"type({celltype})" - else: - new_value = ModbusSimulatorContext.build_value_from_registers( - regs, is_int - ) - assert expect_value == new_value, f"type({celltype})" + + assert expect_value == reg_value, f"type({CellType(celltype).name})" @pytest.mark.parametrize( ("celltype", "minval", "maxval"), @@ -513,6 +548,8 @@ async def test_simulator_action_increment( (CellType.UINT32, 50, 63075), (CellType.FLOAT32, 27.0, 16100.5), (CellType.FLOAT32, 65.0, 78.0), + (CellType.FLOAT64, 78.5, 124.3), + (CellType.FLOAT64, 125.7, 354.2), ], ) async def test_simulator_action_random(self, celltype, minval, maxval, device): @@ -527,8 +564,8 @@ async def test_simulator_action_random(self, celltype, minval, maxval, device): exc_simulator.registers[30].action = action exc_simulator.registers[30].action_parameters = parameters exc_simulator.registers[31].type = CellType.NEXT - is_int = celltype != CellType.FLOAT32 - reg_count = 1 if celltype in (CellType.BITS, CellType.UINT16) else 2 + exc_simulator.registers[31].action = 0 + reg_count = CellType.register_count(celltype) for _i in range(100): if celltype != CellType.BITS: regs = await exc_simulator.async_OLD_getValues( @@ -544,17 +581,41 @@ async def test_simulator_action_random(self, celltype, minval, maxval, device): new_value = regs[0] else: new_value = ModbusSimulatorContext.build_value_from_registers( - regs, is_int + regs, celltype ) assert minval <= new_value <= maxval def test_simulator_loop_validate(self, simulator): """Test simulator set values.""" - assert not simulator.loop_validate(51, 52, False) + assert not simulator.loop_validate(59, 60, False) simulator.type_exception = True assert simulator.loop_validate(5, 6, False) assert not simulator.loop_validate(46, 47, False) - assert simulator.loop_validate(43, 45, False) + assert simulator.loop_validate(51, 53, False) assert not simulator.loop_validate(45, 50, False) assert not simulator.loop_validate(21, 22, False) assert simulator.loop_validate(21, 23, False) + + @pytest.mark.parametrize( + ("celltype", "count", "is_int", "is_64"), + [ + (CellType.INVALID, -1, False, False), + (CellType.BITS, 1, True, False), + (CellType.UINT16, 1, True, False), + (CellType.UINT32, 2, True, False), + (CellType.FLOAT32, 2, False, False), + (CellType.FLOAT64, 4, False, True), + (CellType.STRING, -1, False, False), + (CellType.NEXT, -1, False, False), + ], + ) + def test_cell_type(self, celltype: CellType, count: int, is_int: bool, is_64: bool): + """Test CellType methods.""" + if count < 0: + with pytest.raises(RuntimeError): + CellType.register_count(celltype) + else: + assert CellType.register_count(celltype) == count + + assert CellType.is_int(celltype) == is_int + assert CellType.is_64(celltype) == is_64 diff --git a/test/server/test_simulator.py b/test/server/test_simulator.py index 91b5f5372..0d792c049 100644 --- a/test/server/test_simulator.py +++ b/test/server/test_simulator.py @@ -37,6 +37,7 @@ class TestSimulator: "uint16": 1, "uint32": 45000, "float32": 127.4, + "float64": -42.15, "string": "X", }, "action": { @@ -44,6 +45,7 @@ class TestSimulator: "uint16": None, "uint32": None, "float32": None, + "float64": None, "string": None, }, }, @@ -93,11 +95,12 @@ class TestSimulator: {"addr": [35, 38], "value": 5678.19}, {"addr": [39, 42], "value": 345000.18, "action": "increment"}, ], + "float64": [{"addr": [43, 46], "value": -321.45, "action": "increment"}], "string": [ - {"addr": [43, 44], "value": "Str"}, - {"addr": [45, 48], "value": "Strxyz12"}, + {"addr": [47, 48], "value": "Str"}, + {"addr": [49, 52], "value": "Strxyz12"}, ], - "repeat": [{"addr": [0, 48], "to": [49, 147]}], + "repeat": [{"addr": [0, 52], "to": [53, 159]}], } default_server = { @@ -295,10 +298,10 @@ async def test_simulator_server_string(self, simulator_server, use_port): """Test simulator server end to end.""" client = AsyncModbusTcpClient(NULLMODEM_HOST, port=use_port) assert await client.connect() - result = await client.read_holding_registers(43, count=2, device_id=1) + result = await client.read_holding_registers(47, count=2, device_id=1) assert result.registers[0] == int.from_bytes(bytes("St", "utf-8"), "big") assert result.registers[1] == int.from_bytes(bytes("r ", "utf-8"), "big") - result = await client.read_holding_registers(43, count=6, device_id=1) + result = await client.read_holding_registers(47, count=6, device_id=1) assert result.registers[0] == int.from_bytes(bytes("St", "utf-8"), "big") assert result.registers[1] == int.from_bytes(bytes("r ", "utf-8"), "big") assert result.registers[2] == int.from_bytes(bytes("St", "utf-8"), "big") diff --git a/test/server/test_simulator_api.py b/test/server/test_simulator_api.py index 752870983..362232b4e 100644 --- a/test/server/test_simulator_api.py +++ b/test/server/test_simulator_api.py @@ -38,6 +38,7 @@ class TestSimulatorApi: "uint16": 1, "uint32": 45000, "float32": 127.4, + "float64": 42.43, "string": "X", }, "action": { @@ -45,6 +46,7 @@ class TestSimulatorApi: "uint16": None, "uint32": None, "float32": None, + "float64": None, "string": None, }, }, @@ -94,11 +96,14 @@ class TestSimulatorApi: {"addr": [35, 38], "value": 5678.19}, {"addr": [39, 42], "value": 345000.18, "action": "increment"}, ], + "float64": [ + {"addr": [43, 46], "value": -3.145142}, + ], "string": [ - {"addr": [43, 44], "value": "Str"}, - {"addr": [45, 48], "value": "Strxyz12"}, + {"addr": [47, 48], "value": "Str"}, + {"addr": [49, 52], "value": "Strxyz12"}, ], - "repeat": [{"addr": [0, 48], "to": [49, 147]}], + "repeat": [{"addr": [0, 58], "to": [59, 175]}], } }, }