From 63d1062f01cd50f4747e0a673db0c3873b483b02 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 1 Aug 2024 09:02:32 +0200 Subject: [PATCH 01/17] Prepare dev. --- MAKE_RELEASE.rst | 1 + pymodbus/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MAKE_RELEASE.rst b/MAKE_RELEASE.rst index 9bed9c862..fd8660794 100644 --- a/MAKE_RELEASE.rst +++ b/MAKE_RELEASE.rst @@ -58,3 +58,4 @@ Architecture documentation. ------------------------------------------------------------ * install graphviz * pyreverse -k -o jpg pymodbus +l \ No newline at end of file diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 2a5de0535..91867a341 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -18,5 +18,5 @@ from pymodbus.pdu import ExceptionResponse -__version__ = "3.7.0" +__version__ = "3.7.1dev" __version_full__ = f"[pymodbus, version {__version__}]" From c7ed02b505b2705cd189e484bacff6854053a7d9 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 1 Aug 2024 16:11:31 +0200 Subject: [PATCH 02/17] Update simulator tests. (#2276) --- test/conftest.py | 1 + test/sub_server/test_simulator.py | 257 +++++++++++++++++------------- 2 files changed, 149 insertions(+), 109 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 3cc85989f..7becad60d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -46,6 +46,7 @@ def pytest_configure(): "TestClientServerSyncExamples": 8300, "TestClientServerAsyncExamples": 8400, "TestNetwork": 8500, + "TestSimulator": 8600, } diff --git a/test/sub_server/test_simulator.py b/test/sub_server/test_simulator.py index a870d942e..b05d81432 100644 --- a/test/sub_server/test_simulator.py +++ b/test/sub_server/test_simulator.py @@ -7,6 +7,7 @@ import pytest +from pymodbus.client import AsyncModbusTcpClient from pymodbus.datastore import ModbusSimulatorContext from pymodbus.datastore.simulator import Cell, CellType, Label from pymodbus.server import ModbusSimulatorServer @@ -22,8 +23,7 @@ class TestSimulator: """Unittest for the pymodbus.Simutor module.""" - simulator = None - default_config = { + default_device = { "setup": { "co size": 100, "di size": 150, @@ -100,7 +100,7 @@ class TestSimulator: "repeat": [{"addr": [0, 48], "to": [49, 147]}], } - default_server_config = { + default_server = { "server": { "comm": "tcp", "host": NULLMODEM_HOST, @@ -173,6 +173,13 @@ class TestSimulator: # 48 MAX before repeat ] + @staticmethod + @pytest.fixture(name="use_port") + def get_port_in_class(base_ports): + """Return next port.""" + base_ports[__class__.__name__] += 1 + return base_ports[__class__.__name__] + @classmethod def custom_action1(cls, _inx, _cell): """Test action.""" @@ -186,10 +193,44 @@ def custom_action2(cls, _inx, _cell): "custom2": custom_action2, } - def setup_method(self): - """Do simulator test setup.""" - test_setup = copy.deepcopy(self.default_config) - self.simulator = ModbusSimulatorContext(test_setup, self.custom_actions) + @pytest.fixture(name="device") + def copy_default_device(self): + """Copy default device.""" + return copy.deepcopy(self.default_device) + + @pytest.fixture(name="simulator") + def create_simulator(self, device): + """Create simulator context.""" + return ModbusSimulatorContext(device, self.custom_actions) + + @pytest.fixture(name="server") + def copy_default_server(self, use_port): + """Create simulator context.""" + server = copy.deepcopy(self.default_server) + server["server"]["port"] = use_port + return server + + @pytest.fixture(name="simulator_server") + async def setup_simulator_server(self, server, device, unused_tcp_port): + """Mock open for simulator server.""" + with patch( + "builtins.open", + mock_open( + read_data=json.dumps( + { + "server_list": server, + "device_list": {"device": device}, + } + ) + ) + ): + task = ModbusSimulatorServer(http_port=unused_tcp_port) + await task.run_forever(only_start=True) + await asyncio.sleep(0.5) + task_future = task.serving + yield task + await task.stop() + await task_future def test_pack_unpack_values(self): """Test the pack unpack methods.""" @@ -203,13 +244,13 @@ def test_pack_unpack_values(self): test_value = ModbusSimulatorContext.build_value_from_registers(regs, False) assert round(value, 6) == round(test_value, 6) - def test_simulator_config_verify(self): + def test_simulator_config_verify(self, simulator): """Test basic configuration.""" # Manually build expected memory image and then compare. - assert self.simulator.register_count == 250 + assert simulator.register_count == 250 for offset in (0, 49, 98): for i, test_cell in enumerate(self.test_registers): - reg = self.simulator.registers[i + offset] + reg = simulator.registers[i + offset] assert reg.type == test_cell.type, f"at index {i} - {offset}" assert reg.access == test_cell.access, f"at index {i} - {offset}" assert reg.value == test_cell.value, f"at index {i} - {offset}" @@ -224,85 +265,97 @@ def test_simulator_config_verify(self): reg.count_write == test_cell.count_write ), f"at index {i} - {offset}" - def test_simulator_config_verify2(self): + def test_simulator_config_verify2(self, device): """Test basic configuration.""" # 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 - del exc_setup[Label.repeat] - exc_setup[Label.repeat] = [] - simulator = ModbusSimulatorContext(exc_setup, None) + device[Label.setup][Label.shared_blocks] = False + device[Label.setup][Label.co_size] = 15 + device[Label.setup][Label.di_size] = 15 + device[Label.setup][Label.hr_size] = 15 + device[Label.setup][Label.ir_size] = 15 + del device[Label.repeat] + device[Label.repeat] = [] + simulator = ModbusSimulatorContext(device, None) assert simulator.register_count == 60 for i, test_cell in enumerate(self.test_registers): reg = simulator.registers[i] assert reg.type == test_cell.type, f"at index {i}" assert reg.value == test_cell.value, f"at index {i}" - def test_simulator_invalid_config(self): + def test_simulator_invalid_config1(self, device): """Test exception for invalid configuration.""" - exc_setup = copy.deepcopy(self.default_config) - exc_setup["bad section"] = True + device["bad section"] = True with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - for entry in ( + ModbusSimulatorContext(device, None) + + @pytest.mark.parametrize( + ("entry"), + [ (Label.type_bits, 5), (Label.type_uint16, 16), (Label.type_uint32, [31, 32]), (Label.type_float32, [33, 34]), (Label.type_string, [43, 44]), - ): - exc_setup = copy.deepcopy(self.default_config) - exc_setup[entry[0]].append(entry[1]) - with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - exc_setup = copy.deepcopy(self.default_config) - del exc_setup[Label.type_bits] + ], + ) + def test_simulator_invalid_config2(self, entry, device): + """Test exception for invalid configuration.""" + device[entry[0]].append(entry[1]) with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - exc_setup = copy.deepcopy(self.default_config) - exc_setup[Label.type_string][1][Label.value] = "very long string again" + ModbusSimulatorContext(device, None) + + def test_simulator_invalid_config3(self, device): + """Test exception for invalid configuration.""" + del device[Label.type_bits] with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - exc_setup = copy.deepcopy(self.default_config) - exc_setup[Label.setup][Label.defaults][Label.action][ + ModbusSimulatorContext(device, None) + + def test_simulator_invalid_config4(self, device): + """Test exception for invalid configuration.""" + device[Label.type_string][1][Label.value] = "very long string again" + with pytest.raises(RuntimeError): + ModbusSimulatorContext(device, None) + + def test_simulator_invalid_config5(self, device): + """Test exception for invalid configuration.""" + device[Label.setup][Label.defaults][Label.action][ Label.type_bits ] = "bad action" with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - exc_setup = copy.deepcopy(self.default_config) - exc_setup[Label.invalid].append(700) - with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - exc_setup = copy.deepcopy(self.default_config) - exc_setup[Label.write].append(700) - with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - exc_setup = copy.deepcopy(self.default_config) - exc_setup[Label.write].append(1) + ModbusSimulatorContext(device, None) + + def test_simulator_invalid_config6(self, device): + """Test exception for invalid configuration.""" + device[Label.invalid].append(700) with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - exc_setup = copy.deepcopy(self.default_config) - exc_setup[Label.type_bits].append(700) + ModbusSimulatorContext(device, None) + + @pytest.mark.parametrize(("entry"), [700, 1]) + def test_simulator_invalid_config7(self, entry, device): + """Test exception for invalid configuration.""" + device[Label.write].append(entry) with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - exc_setup = copy.deepcopy(self.default_config) - exc_setup[Label.repeat][0][Label.repeat_to] = [48, 500] + ModbusSimulatorContext(device, None) + + def test_simulator_invalid_config8(self, device): + """Test exception for invalid configuration.""" + device[Label.type_bits].append(700) with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) - exc_setup = copy.deepcopy(self.default_config) - 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) + ModbusSimulatorContext(device, None) + + def test_simulator_invalid_config9(self, device): + """Test exception for invalid configuration.""" + device[Label.repeat][0][Label.repeat_to] = [48, 500] with pytest.raises(RuntimeError): - ModbusSimulatorContext(exc_setup, None) + ModbusSimulatorContext(device, None) + def test_simulator_invalid_config10(self, device): + """Test exception for invalid configuration.""" + device[Label.type_uint16].append(250) + with pytest.raises(RuntimeError): + ModbusSimulatorContext(device, None) - def test_simulator_validate_illegal(self): + def test_simulator_validate_illegal(self, simulator): """Test validation without exceptions.""" illegal_cell_list = (0, 1, 2, 3, 4, 6, 9, 15) write_cell_list = ( @@ -328,8 +381,8 @@ def test_simulator_validate_illegal(self): # for func_code in (FX_READ_BIT, FX_READ_REG, FX_WRITE_BIT, FX_WRITE_REG): for func_code in (FX_READ_BIT,): for addr in range(len(self.test_registers) - 1): - exp1 = self.simulator.validate(func_code, addr * 16, 1) - exp2 = self.simulator.validate(func_code, addr * 16, 20) + exp1 = simulator.validate(func_code, addr * 16, 1) + exp2 = simulator.validate(func_code, addr * 16, 20) # Illegal cell and no write if addr in illegal_cell_list: assert not exp1, f"wrong illegal at index {addr}" @@ -347,14 +400,13 @@ def test_simulator_validate_illegal(self): assert exp1, f"wrong legal at index {addr}" assert exp2, f"wrong legal at second index {addr+1}" addr = 27 - exp1 = self.simulator.validate(FX_WRITE_REG, addr, 1) + exp1 = simulator.validate(FX_WRITE_REG, addr, 1) assert not exp1, f"wrong legal at index {addr}" - def test_simulator_validate_type(self): + def test_simulator_validate_type(self, device): """Test validate call.""" - exc_setup = copy.deepcopy(self.default_config) - exc_setup["setup"]["type exception"] = True - exc_simulator = ModbusSimulatorContext(exc_setup, None) + device["setup"]["type exception"] = True + exc_simulator = ModbusSimulatorContext(device, None) for entry in ( (FX_READ_BIT, 80, 1, True), @@ -372,7 +424,7 @@ def test_simulator_validate_type(self): validated = exc_simulator.validate(entry[0], entry[1], entry[2]) assert entry[3] == validated, f"at entry {entry}" - def test_simulator_get_values(self): + def test_simulator_get_values(self, simulator): """Test simulator get values.""" for entry in ( (FX_READ_BIT, 194, 1, [False]), @@ -382,13 +434,12 @@ def test_simulator_get_values(self): (FX_READ_REG, 19, 1, [14662]), (FX_READ_REG, 16, 2, [3124, 5678]), ): - values = self.simulator.getValues(entry[0], entry[1], entry[2]) + values = simulator.getValues(entry[0], entry[1], entry[2]) assert entry[3] == values, f"at entry {entry}" - def test_simulator_set_values(self): + def test_simulator_set_values(self, device): """Test simulator set values.""" - exc_setup = copy.deepcopy(self.default_config) - exc_simulator = ModbusSimulatorContext(exc_setup, None) + exc_simulator = ModbusSimulatorContext(device, None) value = [31234] exc_simulator.setValues(FX_WRITE_REG, 16, value) result = exc_simulator.getValues(FX_READ_REG, 16, 1) @@ -410,7 +461,7 @@ def test_simulator_set_values(self): assert result == [True, False, False] exc_simulator.setValues(FX_WRITE_BIT, 80, [True] * 17) - def test_simulator_get_text(self): + def test_simulator_get_text(self, simulator): """Test get_text_register().""" for test_reg, test_entry, test_cell in ( (1, "1", Cell(type=Label.invalid, action="none", value="0")), @@ -427,8 +478,8 @@ def test_simulator_get_text(self): (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 ")), ): - reg = self.simulator.registers[test_reg] - entry, cell = self.simulator.get_text_register(test_reg) + reg = simulator.registers[test_reg] + entry, cell = simulator.get_text_register(test_reg) assert entry == test_entry, f"at register {test_reg}" assert cell.type == test_cell.type, f"at register {test_reg}" assert cell.access == str(reg.access), f"at register {test_reg}" @@ -454,10 +505,9 @@ def test_simulator_get_text(self): Label.uptime, ], ) - def test_simulator_actions(self, func, addr, action): + def test_simulator_actions(self, func, addr, action, device): """Test actions.""" - exc_setup = copy.deepcopy(self.default_config) - exc_simulator = ModbusSimulatorContext(exc_setup, None) + exc_simulator = ModbusSimulatorContext(device, None) reg1 = exc_simulator.registers[addr] reg2 = exc_simulator.registers[addr + 1] reg1.action = exc_simulator.action_name_to_id[action] @@ -468,20 +518,18 @@ def test_simulator_actions(self, func, addr, action): values = exc_simulator.getValues(func, addr, 2) assert values[0] or values[1] - def test_simulator_action_timestamp(self): + def test_simulator_action_timestamp(self, device): """Test action timestamp.""" - exc_setup = copy.deepcopy(self.default_config) - exc_simulator = ModbusSimulatorContext(exc_setup, None) + exc_simulator = ModbusSimulatorContext(device, None) addr = 12 exc_simulator.registers[addr].action = exc_simulator.action_name_to_id[ Label.timestamp ] exc_simulator.getValues(FX_READ_REG, addr, 1) - def test_simulator_action_reset(self): + def test_simulator_action_reset(self, device): """Test action reset.""" - exc_setup = copy.deepcopy(self.default_config) - exc_simulator = ModbusSimulatorContext(exc_setup, None) + exc_simulator = ModbusSimulatorContext(device, None) addr = 12 exc_simulator.registers[addr].action = exc_simulator.action_name_to_id[ Label.reset @@ -503,11 +551,10 @@ def test_simulator_action_reset(self): ], ) def test_simulator_action_increment( - self, celltype, minval, maxval, value, expected + self, celltype, minval, maxval, value, expected, device ): """Test action increment.""" - exc_setup = copy.deepcopy(self.default_config) - exc_simulator = ModbusSimulatorContext(exc_setup, None) + exc_simulator = ModbusSimulatorContext(device, None) action = exc_simulator.action_name_to_id[Label.increment] parameters = { "minval": minval, @@ -552,10 +599,9 @@ def test_simulator_action_increment( (CellType.FLOAT32, 65.0, 78.0), ], ) - def test_simulator_action_random(self, celltype, minval, maxval): + def test_simulator_action_random(self, celltype, minval, maxval, device): """Test action random.""" - exc_setup = copy.deepcopy(self.default_config) - exc_simulator = ModbusSimulatorContext(exc_setup, None) + exc_simulator = ModbusSimulatorContext(device, None) action = exc_simulator.action_name_to_id[Label.random] parameters = { "minval": minval, @@ -582,20 +628,13 @@ def test_simulator_action_random(self, celltype, minval, maxval): ) assert minval <= new_value <= maxval - @patch( - "builtins.open", - mock_open( - read_data=json.dumps( - { - "server_list": default_server_config, - "device_list": {"device": default_config}, - } - ) - ), - ) - async def test_simulator_server_tcp(self, unused_tcp_port): + async def test_simulator_server_tcp(self, simulator_server): """Test init simulator server.""" - task = ModbusSimulatorServer(http_port=unused_tcp_port) - await task.run_forever(only_start=True) - await asyncio.sleep(0.5) - await task.stop() + + + async def test_simulator_server_end_to_end(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(16, 1) + assert result.registers[0] == 3124 From a239fc3507b2a66e61425b7eeb499fb593e8cef0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 1 Aug 2024 16:41:30 +0200 Subject: [PATCH 03/17] Proof for issue 2273. (#2277) --- test/sub_server/test_simulator.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/sub_server/test_simulator.py b/test/sub_server/test_simulator.py index b05d81432..7c5019708 100644 --- a/test/sub_server/test_simulator.py +++ b/test/sub_server/test_simulator.py @@ -638,3 +638,22 @@ async def test_simulator_server_end_to_end(self, simulator_server, use_port): assert await client.connect() result = await client.read_holding_registers(16, 1) assert result.registers[0] == 3124 + client.close() + + 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, 2) + 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, 6) + 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") + assert result.registers[3] == int.from_bytes(bytes("rx", "utf-8"), "big") + assert result.registers[4] == int.from_bytes(bytes("yz", "utf-8"), "big") + assert result.registers[5] == int.from_bytes(bytes("12", "utf-8"), "big") + result = await client.read_holding_registers(21, 23) + assert len(result.registers) == 23 + client.close() From 388ab660ca5e57e27144624ec02bcf7b6df976e4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 1 Aug 2024 20:29:56 +0200 Subject: [PATCH 04/17] Add more testing for WriteRegisters. (#2280) --- examples/client_async_calls.py | 5 +++++ pymodbus/client/mixin.py | 4 ++-- test/sub_function_codes/test_register_write_messages.py | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/client_async_calls.py b/examples/client_async_calls.py index bf4d66b43..6456006f5 100755 --- a/examples/client_async_calls.py +++ b/examples/client_async_calls.py @@ -137,6 +137,11 @@ async def async_handle_holding_registers(client): assert not rr.isError() # test that call was OK assert rr.registers == [10] * 8 + await client.write_registers(1, [10], slave=SLAVE) + rr = await client.read_holding_registers(1, 1, slave=SLAVE) + assert not rr.isError() # test that call was OK + assert rr.registers == [10] + _logger.info("### write read holding registers") arguments = { "read_address": 1, diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index e7e5c137b..45f563a0c 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -322,11 +322,11 @@ def write_coils( ) def write_registers( - self, address: int, values: list[int] | int, slave: int = 1, skip_encode: bool = False) -> T: + self, address: int, values: list[int], slave: int = 1, skip_encode: bool = False) -> T: """Write registers (code 0x10). :param address: Start address to write to - :param values: List of values to write, or a single value to write + :param values: List of values to write :param slave: (optional) Modbus slave ID :param skip_encode: (optional) do not encode values :raises ModbusException: diff --git a/test/sub_function_codes/test_register_write_messages.py b/test/sub_function_codes/test_register_write_messages.py index 1ec6f4202..8d702fb77 100644 --- a/test/sub_function_codes/test_register_write_messages.py +++ b/test/sub_function_codes/test_register_write_messages.py @@ -122,6 +122,10 @@ async def test_write_multiple_register_request(self): result = await request.execute(context) assert result.function_code == request.function_code + request = WriteMultipleRegistersRequest(0x00, [0x00]) + result = await request.execute(context) + assert result.function_code == request.function_code + # -----------------------------------------------------------------------# # Mask Write Register Request # -----------------------------------------------------------------------# From dfef3721d00804764036ede7255a5e69e6ce3c0c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 7 Aug 2024 12:12:00 +0200 Subject: [PATCH 05/17] Explain version schema (#2284) --- README.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.rst b/README.rst index 30a21ee60..e734d81c2 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,18 @@ PyModbus - A Python Modbus Stack Pymodbus is a full Modbus protocol implementation offering client/server with synchronous/asynchronous API a well as simulators. +Our releases is defined as X.Y.Z, and we have strict rules what to release when: + +- **Z**, No API changes! bug fixes and smaller enhancements. +- **Y**, API changes, bug fixes and bigger enhancements. +- **X**, Major changes in API and/or method to use pymodbus + +Upgrade examples: + +- 3.6.1 -> 3.6.9: just plugin the new version, no changes needed. +- 3.6.1 -> 3.7.0: Smaller changes to the pymodbus calls might be needed +- 2.5.4 -> 3.0.0: Major changes in the application might be needed + Current release is `3.7.0 `_. Bleeding edge (not released) is `dev `_. From 4b83e59137a8b577b946637e297f601765e71ac2 Mon Sep 17 00:00:00 2001 From: Alex <52292902+alexrudd2@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:39:19 -0500 Subject: [PATCH 06/17] example docstrings diag_message -> pdu.diag_message (#2286) --- examples/client_async_calls.py | 4 ++-- examples/client_calls.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/client_async_calls.py b/examples/client_async_calls.py index 6456006f5..3f58c5673 100755 --- a/examples/client_async_calls.py +++ b/examples/client_async_calls.py @@ -13,8 +13,8 @@ If you are performing a request that is not available in the client mixin, you have to perform the request like this instead:: - from pymodbus.diag_message import ClearCountersRequest - from pymodbus.diag_message import ClearCountersResponse + from pymodbus.pdu.diag_message import ClearCountersRequest + from pymodbus.pdu.diag_message import ClearCountersResponse request = ClearCountersRequest() response = client.execute(request) diff --git a/examples/client_calls.py b/examples/client_calls.py index c1de95f93..3b3971fdf 100755 --- a/examples/client_calls.py +++ b/examples/client_calls.py @@ -13,8 +13,8 @@ If you are performing a request that is not available in the client mixin, you have to perform the request like this instead:: - from pymodbus.diag_message import ClearCountersRequest - from pymodbus.diag_message import ClearCountersResponse + from pymodbus.pdu.diag_message import ClearCountersRequest + from pymodbus.pdu.diag_message import ClearCountersResponse request = ClearCountersRequest() response = client.execute(request) From 2b2dd06a6b84f3c9aa42be93f048c8b3acfb154d Mon Sep 17 00:00:00 2001 From: Alex <52292902+alexrudd2@users.noreply.github.com> Date: Fri, 9 Aug 2024 00:44:13 -0500 Subject: [PATCH 07/17] Clean up ModbusControlBlock (#2288) --- pymodbus/device.py | 68 +++++++++++++++++------------------- pymodbus/pdu/diag_message.py | 4 +-- test/test_device.py | 10 +++--- 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/pymodbus/device.py b/pymodbus/device.py index adcc58296..61d97e8db 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -444,14 +444,14 @@ class ModbusControlBlock: should come from here. """ - __mode = "ASCII" - __diagnostic = [False] * 16 - __listen_only = False - __delimiter = b"\r" - __counters = ModbusCountersHandler() - __identity = ModbusDeviceIdentification() - __plus = ModbusPlusStatistics() - __events: list[ModbusEvent] = [] + _mode = "ASCII" + _diagnostic = [False] * 16 + _listen_only = False + _delimiter = b"\r" + _counters = ModbusCountersHandler() + _identity = ModbusDeviceIdentification() + _plus = ModbusPlusStatistics() + _events: list[ModbusEvent] = [] # -------------------------------------------------------------------------# # Magic @@ -468,7 +468,7 @@ def __iter__(self): :returns: An iterator of the device counters """ - return self.__counters.__iter__() + return self._counters.__iter__() def __new__(cls): """Create a new instance.""" @@ -484,8 +484,8 @@ def addEvent(self, event: ModbusEvent): :param event: A new event to add to the log """ - self.__events.insert(0, event) - self.__events = self.__events[0:64] # chomp to 64 entries + self._events.insert(0, event) + self._events = self._events[0:64] # chomp to 64 entries self.Counter.Event += 1 def getEvents(self): @@ -493,26 +493,26 @@ def getEvents(self): :returns: The encoded events packet """ - events = [event.encode() for event in self.__events] + events = [event.encode() for event in self._events] return b"".join(events) def clearEvents(self): """Clear the current list of events.""" - self.__events = [] + self._events = [] # -------------------------------------------------------------------------# # Other Properties # -------------------------------------------------------------------------# - Identity = property(lambda s: s.__identity) - Counter = property(lambda s: s.__counters) - Events = property(lambda s: s.__events) - Plus = property(lambda s: s.__plus) + Identity = property(lambda s: s._identity) + Counter = property(lambda s: s._counters) + Events = property(lambda s: s._events) + Plus = property(lambda s: s._plus) def reset(self): """Clear all of the system counters and the diagnostic register.""" - self.__events = [] - self.__counters.reset() - self.__diagnostic = [False] * 16 + self._events = [] + self._counters.reset() + self._diagnostic = [False] * 16 # -------------------------------------------------------------------------# # Listen Properties @@ -522,9 +522,9 @@ def _setListenOnly(self, value): :param value: The value to set the listen status to """ - self.__listen_only = bool(value) # pylint: disable=unused-private-member + self._listen_only = bool(value) - ListenOnly = property(lambda s: s.__listen_only, _setListenOnly) + ListenOnly = property(lambda s: s._listen_only, _setListenOnly) # -------------------------------------------------------------------------# # Mode Properties @@ -535,9 +535,9 @@ def _setMode(self, mode): :param mode: The data transfer method in (RTU, ASCII) """ if mode in {"ASCII", "RTU"}: - self.__mode = mode # pylint: disable=unused-private-member + self._mode = mode - Mode = property(lambda s: s.__mode, _setMode) + Mode = property(lambda s: s._mode, _setMode) # -------------------------------------------------------------------------# # Delimiter Properties @@ -548,15 +548,13 @@ def _setDelimiter(self, char): :param char: The new serial delimiter character """ if isinstance(char, str): - self.__delimiter = char.encode() # pylint: disable=unused-private-member + self._delimiter = char.encode() if isinstance(char, bytes): - self.__delimiter = char # pylint: disable=unused-private-member + self._delimiter = char elif isinstance(char, int): - self.__delimiter = struct.pack( # pylint: disable=unused-private-member - ">B", char - ) + self._delimiter = struct.pack(">B", char) - Delimiter = property(lambda s: s.__delimiter, _setDelimiter) + Delimiter = property(lambda s: s._delimiter, _setDelimiter) # -------------------------------------------------------------------------# # Diagnostic Properties @@ -567,8 +565,8 @@ def setDiagnostic(self, mapping): :param mapping: Dictionary of key:value pairs to set """ for entry in iter(mapping.items()): - if entry[0] >= 0 and entry[0] < len(self.__diagnostic): - self.__diagnostic[entry[0]] = bool(entry[1]) + if entry[0] >= 0 and entry[0] < len(self._diagnostic): + self._diagnostic[entry[0]] = bool(entry[1]) def getDiagnostic(self, bit): """Get the value in the diagnostic register. @@ -577,8 +575,8 @@ def getDiagnostic(self, bit): :returns: The current value of the requested bit """ try: - if bit and 0 <= bit < len(self.__diagnostic): - return self.__diagnostic[bit] + if bit and 0 <= bit < len(self._diagnostic): + return self._diagnostic[bit] except Exception: # pylint: disable=broad-except return None return None @@ -588,4 +586,4 @@ def getDiagnosticRegister(self): :returns: The diagnostic register collection """ - return self.__diagnostic + return self._diagnostic diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index de78c2def..e23b56a68 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -345,7 +345,7 @@ async def execute(self, *args): :returns: The initialized response message """ char = (self.message & 0xFF00) >> 8 # type: ignore[operator] - _MCB._setDelimiter(char) # pylint: disable=protected-access + _MCB.Delimiter = char return ChangeAsciiInputDelimiterResponse(self.message) @@ -379,7 +379,7 @@ async def execute(self, *args): :returns: The initialized response message """ - _MCB._setListenOnly(True) # pylint: disable=protected-access + _MCB.ListenOnly = True return ForceListenOnlyModeResponse() diff --git a/test/test_device.py b/test/test_device.py index 5fb6d1c48..e150068d8 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -21,9 +21,9 @@ class TestDataStore: # Setup/TearDown # -----------------------------------------------------------------------# - info = None - ident = None - control = None + info: dict + ident: ModbusDeviceIdentification + control: ModbusControlBlock def setup_method(self): """Do setup.""" @@ -255,9 +255,9 @@ def test_modbus_control_block_delimiter(self): self.control.Delimiter = b"\r" assert self.control.Delimiter == b"\r" self.control.Delimiter = "=" - assert self.control.Delimiter == b"=" + assert self.control.Delimiter == b"=" # type: ignore[comparison-overlap] self.control.Delimiter = 61 - assert self.control.Delimiter == b"=" + assert self.control.Delimiter == b"=" # type: ignore[comparison-overlap] def test_modbus_control_block_diagnostic(self): """Tests the MCB delimiter setting methods.""" From db2e71f58f789dc27745a43a13e5a44452e90fbf Mon Sep 17 00:00:00 2001 From: Alex <52292902+alexrudd2@users.noreply.github.com> Date: Fri, 9 Aug 2024 00:46:08 -0500 Subject: [PATCH 08/17] Simplify framer test setup (#2290) --- test/framers/conftest.py | 45 +++++++--------------------------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/test/framers/conftest.py b/test/framers/conftest.py index 078599c99..e5cb9304d 100644 --- a/test/framers/conftest.py +++ b/test/framers/conftest.py @@ -7,35 +7,7 @@ from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.framer import Framer, FramerType -from pymodbus.transport import CommParams, ModbusProtocol - - -class DummyFramer(Framer): - """Implement use of ModbusProtocol.""" - - def __init__(self, - framer_type: FramerType, - params: CommParams, - is_server: bool, - device_ids: list[int] | None, - ): - """Initialize a frame instance.""" - super().__init__(framer_type, params, is_server, device_ids) - self.send = mock.Mock() - self.framer_type = framer_type - - def callback_new_connection(self) -> ModbusProtocol: - """Call when listener receive new connection request.""" - return DummyFramer(self.framer_type, self.comm_params, self.is_server, self.device_ids) # pragma: no cover - - def callback_connected(self) -> None: - """Call when connection is succcesfull.""" - - def callback_disconnected(self, exc: Exception | None) -> None: - """Call when connection is lost.""" - - def callback_request_response(self, data: bytes, device_id: int, tid: int) -> None: - """Handle received modbus request/response.""" +from pymodbus.transport import CommParams @pytest.fixture(name="entry") @@ -48,19 +20,16 @@ def prepare_is_server(): """Return client/server.""" return False +@mock.patch.multiple(Framer, __abstractmethods__=set()) # eliminate abstract methods (callbacks) @pytest.fixture(name="dummy_framer") async def prepare_test_framer(entry, is_server): """Return framer object.""" - framer = DummyFramer( - entry, - CommParams(), - is_server, - [0, 1], - ) + framer = Framer(entry, CommParams(), is_server, [0, 1]) # type: ignore[abstract] + framer.send = mock.Mock() # type: ignore[method-assign] if entry == FramerType.RTU: - func_table = (ServerDecoder if is_server else ClientDecoder)().lookup + func_table = (ServerDecoder if is_server else ClientDecoder)().lookup # type: ignore[attr-defined] for key, ent in func_table.items(): - fix_len = ent._rtu_frame_size if hasattr(ent, "_rtu_frame_size") else 0 # pylint: disable=protected-access - cnt_pos = ent. _rtu_byte_count_pos if hasattr(ent, "_rtu_byte_count_pos") else 0 # pylint: disable=protected-access + fix_len = getattr(ent, "_rtu_frame_size", 0) + cnt_pos = getattr(ent, "_rtu_byte_count_pos", 0) framer.handle.set_fc_calc(key, fix_len, cnt_pos) return framer From de7f1700c7b59ee12a0f353eea1d86b722e24114 Mon Sep 17 00:00:00 2001 From: Alex <52292902+alexrudd2@users.noreply.github.com> Date: Sun, 11 Aug 2024 11:05:12 -0500 Subject: [PATCH 09/17] Fix aiohttp < 3.9.0 (#2289) --- pymodbus/server/simulator/http_server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 39cc843c2..9e4afb570 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -218,7 +218,8 @@ def __init__( self.request_lookup = ServerDecoder.getFCdict() self.call_monitor = CallTypeMonitor() self.call_response = CallTypeResponse() - self.api_key: web.AppKey = web.AppKey("modbus_server") + app_key = getattr(web, 'AppKey', str) # fall back to str for aiohttp < 3.9.0 + self.api_key = app_key("modbus_server") async def start_modbus_server(self, app): """Start Modbus server as asyncio task.""" From bdbbe9f021deb5506d0a9c70f0014b8d2ead0710 Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Mon, 12 Aug 2024 12:33:01 +0530 Subject: [PATCH 10/17] Update repl requirement to >= 2.0.4 (#2291) Co-authored-by: Alex <52292902+alexrudd2@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 99182bdb5..75f146ed4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ serial = [ "pyserial>=3.5" ] repl = [ - "pymodbus-repl>=2.0.3" + "pymodbus-repl>=2.0.4" ] simulator = [ From 3e60a20cef282fed59281cded7551b9f231a7786 Mon Sep 17 00:00:00 2001 From: Pavel Kostromitinov Date: Tue, 13 Aug 2024 10:53:53 +0300 Subject: [PATCH 11/17] Properly process 'slaves' argument (#2292) --- examples/helper.py | 1 - examples/server_async.py | 25 +++++++------------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/examples/helper.py b/examples/helper.py index d1d93b64d..e675bde54 100755 --- a/examples/helper.py +++ b/examples/helper.py @@ -76,7 +76,6 @@ def get_commandline(server=False, description=None, extras=None, cmdline=None): help="set number of slaves, default is 0 (any)", default=0, type=int, - nargs="+", ) parser.add_argument( "--context", diff --git a/examples/server_async.py b/examples/server_async.py index 6835426d5..433d70157 100755 --- a/examples/server_async.py +++ b/examples/server_async.py @@ -89,7 +89,7 @@ def setup_server(description=None, context=None, cmdline=None): # full address range:: datablock = lambda : ModbusSequentialDataBlock.create() # pylint: disable=unnecessary-lambda-assignment,unnecessary-lambda - if args.slaves: + if args.slaves > 1: # The server then makes use of a server context that allows the server # to respond with different slave contexts for different slave ids. # By default it will return the same context for every slave id supplied @@ -101,27 +101,16 @@ def setup_server(description=None, context=None, cmdline=None): # that a request to address(0-7) will map to the address (0-7). # The default is False which is based on section 4.4 of the # specification, so address(0-7) will map to (1-8):: - context = { - 0x01: ModbusSlaveContext( - di=datablock(), - co=datablock(), - hr=datablock(), - ir=datablock(), - ), - 0x02: ModbusSlaveContext( - di=datablock(), - co=datablock(), - hr=datablock(), - ir=datablock(), - ), - 0x03: ModbusSlaveContext( + context = {} + + for slave in range(args.slaves): + context[slave] = ModbusSlaveContext( di=datablock(), co=datablock(), hr=datablock(), ir=datablock(), - zero_mode=True, - ), - } + ) + single = False else: context = ModbusSlaveContext( From 6a752018fe19442825f5db09d8b413082eb36ae4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Aug 2024 14:20:34 +0200 Subject: [PATCH 12/17] Ruff complains, due to upgrade. (#2296) --- test/framers/test_rtu.py | 2 +- test/sub_client/test_client.py | 4 ++-- test/test_sparse_datastore.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/framers/test_rtu.py b/test/framers/test_rtu.py index 13bdd8770..143a29d3d 100644 --- a/test/framers/test_rtu.py +++ b/test/framers/test_rtu.py @@ -13,7 +13,7 @@ def prepare_frame(): """Return message object.""" return FramerRTU() - @pytest.mark.skip() + @pytest.mark.skip @pytest.mark.parametrize( ("packet", "used_len", "res_id", "res"), [ diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 6111dfb59..f4b6adc2b 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -313,7 +313,7 @@ async def test_client_base_async(): p_close.return_value.set_result(False) -@pytest.mark.skip() +@pytest.mark.skip async def test_client_protocol_receiver(): """Test the client protocol data received.""" base = ModbusBaseClient( @@ -340,7 +340,7 @@ async def test_client_protocol_receiver(): await base.build_response(0x00) # pylint: disable=protected-access -@pytest.mark.skip() +@pytest.mark.skip async def test_client_protocol_response(): """Test the udp client protocol builds responses.""" base = ModbusBaseClient( diff --git a/test/test_sparse_datastore.py b/test/test_sparse_datastore.py index 250489bb8..4e2f71b43 100644 --- a/test/test_sparse_datastore.py +++ b/test/test_sparse_datastore.py @@ -5,7 +5,7 @@ from pymodbus.datastore import ModbusSparseDataBlock -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_check_async_sparsedatastore(): """Test check frame.""" data_in_block = { From fda815fff5faec3a02b5574fc947946d3ad48b0e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 22 Aug 2024 15:09:32 +0200 Subject: [PATCH 13/17] Correct max. read size for registers. (#2295) --- pymodbus/pdu/register_read_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index ee1fd0e1a..c647c2ef0 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -89,7 +89,7 @@ def decode(self, data): :param data: The request to decode """ byte_count = int(data[0]) - if byte_count < 2 or byte_count > 246 or byte_count % 2 == 1 or byte_count != len(data) - 1: + if byte_count < 2 or byte_count > 252 or byte_count % 2 == 1 or byte_count != len(data) - 1: raise ModbusIOException(f"Invalid response {data} has byte count of {byte_count}") self.registers = [] for i in range(1, byte_count + 1, 2): From 3190a5a06dfe0f8ca440577920d78273cf5a0a3e Mon Sep 17 00:00:00 2001 From: efdx <150683046+efdx@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:54:07 +0300 Subject: [PATCH 14/17] Feature/add simulator api skeleton (#2274) Signed-off-by: Esa Laakso Co-authored-by: jan iversen --- .../library/simulator/calls_request.rst | 14 + .../library/simulator/calls_response.rst | 167 +++++++ .../library/simulator/registers_request.rst | 7 + .../library/simulator/registers_response.rst | 34 ++ doc/source/library/simulator/restapi.rst | 118 ++++- pymodbus/server/simulator/http_server.py | 188 ++++++- pyproject.toml | 2 + test/sub_server/test_simulator_api.py | 466 ++++++++++++++++++ 8 files changed, 968 insertions(+), 28 deletions(-) create mode 100644 doc/source/library/simulator/calls_request.rst create mode 100644 doc/source/library/simulator/calls_response.rst create mode 100644 doc/source/library/simulator/registers_request.rst create mode 100644 doc/source/library/simulator/registers_response.rst create mode 100644 test/sub_server/test_simulator_api.py diff --git a/doc/source/library/simulator/calls_request.rst b/doc/source/library/simulator/calls_request.rst new file mode 100644 index 000000000..1e8e274e2 --- /dev/null +++ b/doc/source/library/simulator/calls_request.rst @@ -0,0 +1,14 @@ +.. code-block:: json + + { + "submit": "Simulate" + "response_clear_after": 0, + "response_cr": "", + "response_cr_pct": 0, + "response_split": "", + "split_delay": 1 + "response_delay": 0, + "response_error": 0, + "response_junk_datalen": 0, + "response_type": 0, + } \ No newline at end of file diff --git a/doc/source/library/simulator/calls_response.rst b/doc/source/library/simulator/calls_response.rst new file mode 100644 index 000000000..f3cb303fd --- /dev/null +++ b/doc/source/library/simulator/calls_response.rst @@ -0,0 +1,167 @@ +.. code-block:: json + + { + "simulation_action": "ACTIVE", + "range_start": null, + "range_stop": null, + "function_codes": [ + { + "value": 3, + "text": "read_holding_registers", + "selected": false + }, + { + "value": 2, + "text": "read_discrete_input", + "selected": false + }, + { + "value": 4, + "text": "read_input_registers", + "selected": false + }, + { + "value": 1, + "text": "read_coils", + "selected": false + }, + { + "value": 15, + "text": "write_coils", + "selected": false + }, + { + "value": 16, + "text": "write_registers", + "selected": false + }, + { + "value": 6, + "text": "write_register", + "selected": false + }, + { + "value": 5, + "text": "write_coil", + "selected": false + }, + { + "value": 23, + "text": "read_write_multiple_registers", + "selected": false + }, + { + "value": 8, + "text": "diagnostic_status", + "selected": false + }, + { + "value": 7, + "text": "read_exception_status", + "selected": false + }, + { + "value": 11, + "text": "get_event_counter", + "selected": false + }, + { + "value": 12, + "text": "get_event_log", + "selected": false + }, + { + "value": 17, + "text": "report_slave_id", + "selected": false + }, + { + "value": 20, + "text": "read_file_record", + "selected": false + }, + { + "value": 21, + "text": "write_file_record", + "selected": false + }, + { + "value": 22, + "text": "mask_write_register", + "selected": false + }, + { + "value": 24, + "text": "read_fifo_queue", + "selected": false + }, + { + "value": 43, + "text": "read_device_information", + "selected": false + } + ], + "function_show_hex_checked": false, + "function_show_decoded_checked": false, + "function_response_normal_checked": true, + "function_response_error_checked": false, + "function_response_empty_checked": false, + "function_response_junk_checked": false, + "function_response_split_checked": true, + "function_response_split_delay": 1, + "function_response_cr_checked": false, + "function_response_cr_pct": 0, + "function_response_delay": 0, + "function_response_junk": 0, + "function_error": [ + { + "value": 1, + "text": "IllegalFunction", + "selected": false + }, + { + "value": 2, + "text": "IllegalAddress", + "selected": false + }, + { + "value": 3, + "text": "IllegalValue", + "selected": false + }, + { + "value": 4, + "text": "SlaveFailure", + "selected": false + }, + { + "value": 5, + "text": "Acknowledge", + "selected": false + }, + { + "value": 6, + "text": "SlaveBusy", + "selected": false + }, + { + "value": 7, + "text": "MemoryParityError", + "selected": false + }, + { + "value": 10, + "text": "GatewayPathUnavailable", + "selected": false + }, + { + "value": 11, + "text": "GatewayNoResponse", + "selected": false + } + ], + "function_response_clear_after": 1, + "call_rows": [], + "foot": "not active", + "result": "ok" + } \ No newline at end of file diff --git a/doc/source/library/simulator/registers_request.rst b/doc/source/library/simulator/registers_request.rst new file mode 100644 index 000000000..24e5ece1a --- /dev/null +++ b/doc/source/library/simulator/registers_request.rst @@ -0,0 +1,7 @@ +.. code-block:: json + + { + "range_start": 16, + "range_end": 16, + "submit": "Register" + } \ No newline at end of file diff --git a/doc/source/library/simulator/registers_response.rst b/doc/source/library/simulator/registers_response.rst new file mode 100644 index 000000000..1bdfa70c8 --- /dev/null +++ b/doc/source/library/simulator/registers_response.rst @@ -0,0 +1,34 @@ +.. code-block:: json + + { + "result": "ok", + "footer": "Operation completed successfully", + "register_types": { + "bits": 1, + "uint16": 2, + "uint32": 3, + "float32": 4, + "string": 5, + "next": 6, + "invalid": 0 + }, + "register_actions": { + "null": 0, + "increment": 1, + "random": 2, + "reset": 3, + "timestamp": 4, + "uptime": 5 + }, + "register_rows": [ + { + "index": "16", + "type": "uint16", + "access": "True", + "action": "none", + "value": "3124", + "count_read": "0", + "count_write": "0" + } + ] + } \ No newline at end of file diff --git a/doc/source/library/simulator/restapi.rst b/doc/source/library/simulator/restapi.rst index a976a9e1e..74d9bedfa 100644 --- a/doc/source/library/simulator/restapi.rst +++ b/doc/source/library/simulator/restapi.rst @@ -1,4 +1,120 @@ Pymodbus simulator ReST API =========================== -TO BE DOCUMENTED. +This is still a Work In Progress. There may be large changes to the API in the +future. + +The API is a simple copy of +having most of the same features as in the Web UI. + +The API provides the following endpoints: + +- /restapi/registers +- /restapi/calls +- /restapi/server +- /restapi/log + +Registers Endpoint +------------------ + +/restapi/registers +^^^^^^^^^^^^^^^^^^ + + The registers endpoint is used to read and write registers. + + **Request Parameters** + + - `submit` (string, required): + The action to perform. Must be one of `Register`, `Set`. + - `range_start` (integer, optional): + The starting register to read from. Defaults to 0. + - `range_end` (integer, optional): + The ending register to read from. Defaults to `range_start`. + + **Response Parameters** + + Returns a json object with the following keys: + + - `result` (string): + The result of the action. Either `ok` or `error`. + - `error` (string, conditional): + The error message if the result is `error`. + - `register_rows` (list): + A list of objects containing the data of the registers. + - `footer` (string): + A cleartext status of the action. HTML leftover. + - `register_types` (list): + A static list of register types. HTML leftover. + - `register_actions` (list): + A static list of register actions. HTML leftover. + + **Example Request and Response** + + Request Example: + + .. include:: registers_request.rst + + Response Example: + + .. include:: registers_response.rst + +Calls Endpoint +-------------- + +The calls endpoint is used to handle ModBus response manipulation. + +/restapi/calls +^^^^^^^^^^^^^^ + + The calls endpoint is used to simulate different conditions for ModBus + responses. + + **Request Parameters** + + - `submit` (string, required): + The action to perform. Must be one of `Simulate`, `Reset`. + + The following must be present if `submit` is `Simulate`: + + - `response_clear_after` (integer, required): + The number of packet to clear simulation after. + - `response_cr` (string, required): + Must be present but can be any value. Turns on change rate simulation (WIP). + - `response_cr_pct` (integer, required): + The percentage of change rate, how many percent of packets should be + changed. + - `response_split` (string, required): + Must be present but can be any value. Turns on split response simulation (WIP). + - `split_delay` (integer, required): + The delay in seconds to wait before sending the second part of the split response. + - `response_delay` (integer, required): + The delay in seconds to wait before sending the response. + - `response_error` (integer, required): + The error code to send in the response. The valid values can be one from + the response `function_error` list. + + When `submit` is `Reset`, no other parameters are required. It resets all + simulation options to their defaults (off). + + **Example Request and Response** + + Request: + + .. include:: calls_request.rst + + Response: + + Unfortunately, the endpoint response contains extra clutter due to + not being finalized. + + .. include:: calls_response.rst + +Server Endpoint +--------------- + +The server endpoint has not yet been implemented. + +Log Endpoint +------------ + +The log endpoint has not yet been implemented. \ No newline at end of file diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 9e4afb570..6bf170716 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -180,7 +180,7 @@ def __init__( self.web_app.add_routes( [ web.get("/api/{tail:[a-z]*}", self.handle_html), - web.post("/api/{tail:[a-z]*}", self.handle_json), + web.post("/restapi/{tail:[a-z]*}", self.handle_json), web.get("/{tail:[a-z0-9.]*}", self.handle_html_static), web.get("/", self.handle_html_static), ] @@ -193,13 +193,13 @@ def __init__( "calls": ["", self.build_html_calls], "server": ["", self.build_html_server], } - self.generator_json: dict[str, list] = { - "log_json": [None, self.build_json_log], - "registers_json": [None, self.build_json_registers], - "calls_json": [None, self.build_json_calls], - "server_json": [None, self.build_json_server], + self.generator_json = { + "log": self.build_json_log, + "registers": self.build_json_registers, + "calls": self.build_json_calls, + "server": self.build_json_server, } - self.submit = { + self.submit_html = { "Clear": self.action_clear, "Stop": self.action_stop, "Reset": self.action_reset, @@ -301,15 +301,18 @@ async def handle_html(self, request): async def handle_json(self, request): """Handle api registers.""" - page_type = request.path.split("/")[-1] - params = await request.post() - json_dict = self.generator_html[page_type][0].copy() - result = self.generator_json[page_type][1](params, json_dict) - return web.Response(text=f"json build: {page_type} - {params} - {result}") + command = request.path.split("/")[-1] + params = await request.json() + try: + result = self.generator_json[command](params) + except (KeyError, ValueError, TypeError, IndexError) as exc: + Log.error("Unhandled error during json request: {}", exc) + return web.json_response({"result": "error", "error": f"Unhandled error Error: {exc}"}) + return web.json_response(result) def build_html_registers(self, params, html): """Build html registers page.""" - result_txt, foot = self.helper_build_html_submit(params) + result_txt, foot = self.helper_handle_submit(params, self.submit_html) if not result_txt: result_txt = "ok" if not foot: @@ -354,7 +357,7 @@ def build_html_registers(self, params, html): def build_html_calls(self, params: dict, html: str) -> str: """Build html calls page.""" - result_txt, foot = self.helper_build_html_submit(params) + result_txt, foot = self.helper_handle_submit(params, self.submit_html) if not foot: foot = "Montitoring active" if self.call_monitor.active else "not active" if not result_txt: @@ -461,23 +464,154 @@ def build_html_server(self, _params, html): """Build html server page.""" return html - def build_json_registers(self, params, json_dict): - """Build html registers page.""" - return f"json build registers: {params} - {json_dict}" + def build_json_registers(self, params): + """Build json registers response.""" + # Process params using the helper function + result_txt, foot = self.helper_handle_submit(params, { + "Set": self.action_set, + }) - def build_json_calls(self, params, json_dict): - """Build html calls page.""" - return f"json build calls: {params} - {json_dict}" + if not result_txt: + result_txt = "ok" + if not foot: + foot = "Operation completed successfully" + + # Extract necessary parameters + try: + range_start = int(params.get("range_start", 0)) + range_stop = int(params.get("range_stop", range_start)) + except ValueError: + return {"result": "error", "error": "Invalid range parameters"} + + # Retrieve register details + register_rows = [] + for i in range(range_start, range_stop + 1): + inx, reg = self.datastore_context.get_text_register(i) + row = { + "index": inx, + "type": reg.type, + "access": reg.access, + "action": reg.action, + "value": reg.value, + "count_read": reg.count_read, + "count_write": reg.count_write + } + register_rows.append(row) + + # Generate register types and actions (assume these are predefined mappings) + register_types = dict(self.datastore_context.registerType_name_to_id) + register_actions = dict(self.datastore_context.action_name_to_id) + + # Build the JSON response + json_response = { + "result": result_txt, + "footer": foot, + "register_types": register_types, + "register_actions": register_actions, + "register_rows": register_rows, + } + + return json_response + + def build_json_calls(self, params: dict) -> dict: + """Build json calls response.""" + result_txt, foot = self.helper_handle_submit(params, { + "Reset": self.action_reset, + "Add": self.action_add, + "Simulate": self.action_simulate, + }) + if not foot: + foot = "Monitoring active" if self.call_monitor.active else "not active" + if not result_txt: + result_txt = "ok" + + function_error = [] + for i, txt in ( + (1, "IllegalFunction"), + (2, "IllegalAddress"), + (3, "IllegalValue"), + (4, "SlaveFailure"), + (5, "Acknowledge"), + (6, "SlaveBusy"), + (7, "MemoryParityError"), + (10, "GatewayPathUnavailable"), + (11, "GatewayNoResponse"), + ): + function_error.append({ + "value": i, + "text": txt, + "selected": i == self.call_response.error_response + }) + + range_start = ( + self.call_monitor.range_start + if self.call_monitor.range_start != -1 + else None + ) + range_stop = ( + self.call_monitor.range_stop + if self.call_monitor.range_stop != -1 + else None + ) + + function_codes = [] + for function in self.request_lookup.values(): + function_codes.append({ + "value": function.function_code, # type: ignore[attr-defined] + "text": function.function_code_name, # type: ignore[attr-defined] + "selected": function.function_code == self.call_monitor.function # type: ignore[attr-defined] + }) + + simulation_action = "ACTIVE" if self.call_response.active != RESPONSE_INACTIVE else "" + + max_len = MAX_FILTER if self.call_monitor.active else 0 + while len(self.call_list) > max_len: + del self.call_list[0] + call_rows = [] + for entry in reversed(self.call_list): + call_rows.append({ + "call": entry.call, + "fc": entry.fc, + "address": entry.address, + "count": entry.count, + "data": entry.data.decode() + }) + + json_response = { + "simulation_action": simulation_action, + "range_start": range_start, + "range_stop": range_stop, + "function_codes": function_codes, + "function_show_hex_checked": self.call_monitor.hex, + "function_show_decoded_checked": self.call_monitor.decode, + "function_response_normal_checked": self.call_response.active == RESPONSE_NORMAL, + "function_response_error_checked": self.call_response.active == RESPONSE_ERROR, + "function_response_empty_checked": self.call_response.active == RESPONSE_EMPTY, + "function_response_junk_checked": self.call_response.active == RESPONSE_JUNK, + "function_response_split_checked": self.call_response.split > 0, + "function_response_split_delay": self.call_response.split, + "function_response_cr_checked": self.call_response.change_rate > 0, + "function_response_cr_pct": self.call_response.change_rate, + "function_response_delay": self.call_response.delay, + "function_response_junk": self.call_response.junk_len, + "function_error": function_error, + "function_response_clear_after": self.call_response.clear_after, + "call_rows": call_rows, + "foot": foot, + "result": result_txt + } + + return json_response - def build_json_log(self, params, json_dict): + def build_json_log(self, params): """Build json log page.""" - return f"json build log: {params} - {json_dict}" + return {"result": "error", "error": "log endpoint not implemented", "params": params} - def build_json_server(self, params, json_dict): + def build_json_server(self, params): """Build html server page.""" - return f"json build server: {params} - {json_dict}" + return {"result": "error", "error": "server endpoint not implemented", "params": params} - def helper_build_html_submit(self, params): + def helper_handle_submit(self, params, submit_actions): """Build html register submit.""" try: range_start = int(params.get("range_start", -1)) @@ -487,9 +621,9 @@ def helper_build_html_submit(self, params): range_stop = int(params.get("range_stop", range_start)) except ValueError: range_stop = -1 - if (submit := params["submit"]) not in self.submit: + if (submit := params["submit"]) not in submit_actions: return None, None - return self.submit[submit](params, range_start, range_stop) + return submit_actions[submit](params, range_start, range_stop) def action_clear(self, _params, _range_start, _range_stop): """Clear register filter.""" diff --git a/pyproject.toml b/pyproject.toml index 75f146ed4..4c3bbba50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ development = [ "pytest-profiling>=1.7.0", "pytest-timeout>=2.3.1", "pytest-xdist>=3.6.1", + "pytest-aiohttp>=1.0.5", "ruff>=0.5.3", "twine>=5.1.1", "types-Pygments", @@ -218,6 +219,7 @@ all_files = "1" testpaths = ["test"] addopts = "--cov-report html --durations=10 --dist loadscope --numprocesses auto" asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" timeout = 120 [tool.coverage.run] diff --git a/test/sub_server/test_simulator_api.py b/test/sub_server/test_simulator_api.py new file mode 100644 index 000000000..094134522 --- /dev/null +++ b/test/sub_server/test_simulator_api.py @@ -0,0 +1,466 @@ +"""Test simulator API.""" +import asyncio +import json + +import pytest +from aiohttp import ClientSession + +from pymodbus.server import ModbusSimulatorServer +from pymodbus.server.simulator import http_server + + +class TestSimulatorApi: + """Integration tests for the pymodbus.SimutorServer module.""" + + default_config = { + "server_list": { + "test-device-server": { + # The test does not care about the server configuration, but + # they must be present for the simulator to start. + "comm": "tcp", + "host": "0.0.0.0", + "port": 25020, + "framer": "socket", + } + }, + "device_list": { + "test-device": { + "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, + }, + }, + }, + "invalid": [ + 1, + [3, 4], + ], + "write": [ + 5, + [7, 8], + [16, 18], + [21, 26], + [33, 38], + ], + "bits": [ + 5, + [7, 8], + {"addr": 10, "value": 0x81}, + {"addr": [11, 12], "value": 0x04342}, + {"addr": 13, "action": "random"}, + {"addr": 14, "value": 15, "action": "reset"}, + ], + "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", + "parameters": {"minval": 10, "maxval": 80}, + }, + ], + "float32": [ + {"addr": [33, 34], "value": 3124.5}, + {"addr": [35, 38], "value": 5678.19}, + {"addr": [39, 42], "value": 345000.18, "action": "increment"}, + ], + "string": [ + {"addr": [43, 44], "value": "Str"}, + {"addr": [45, 48], "value": "Strxyz12"}, + ], + "repeat": [{"addr": [0, 48], "to": [49, 147]}], + } + } + } + + # Fixture to set up the aiohttp app + @pytest.fixture + async def client(self, aiohttp_client, tmp_path): + """Fixture to provide usable aiohttp client for testing.""" + async with ClientSession() as session: + yield session + + @pytest.fixture + async def simulator(self, tmp_path): + """Fixture to provide a standard simulator for testing.""" + config_path = tmp_path / "config.json" + # Dump the config to a json file for the simulator + with open(config_path, "w") as file: + json.dump(self.default_config, file) + + simulator = ModbusSimulatorServer( + modbus_server = "test-device-server", + modbus_device = "test-device", + http_host = "localhost", + http_port = 18080, + log_file = "simulator.log", + json_file = config_path + ) + + # Run the simulator in the current event loop. Store the task so they live + # until the test is done. + loop = asyncio.get_running_loop() + task = loop.create_task(simulator.run_forever(only_start=True)) + + # TODO: Make a better way to wait for the simulator to start + await asyncio.sleep(1) + + yield simulator + + # Stop the simulator after the test is done + task.cancel() + await task + await simulator.stop() + + @pytest.mark.asyncio + async def test_registers_json_valid(self, client, simulator): + """Test the /restapi/registers endpoint with valid parameters.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Registers", + "range_start": 16, + "range_stop": 16, + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + + assert "result" in json_response + assert "footer" in json_response + assert "register_types" in json_response + assert "register_actions" in json_response + assert "register_rows" in json_response + + assert json_response["result"] == "ok" + + # Check that we got the correct register and correct fields. Ignore + # the contents of the ones that haven't been explicitly set in + # config, just make sure they are present. + assert json_response["register_rows"][0]["index"] == "16" + assert json_response["register_rows"][0]["type"] == "uint16" + assert json_response["register_rows"][0]["value"] == "3124" + assert "action" in json_response["register_rows"][0] + assert "access" in json_response["register_rows"][0] + assert "count_read" in json_response["register_rows"][0] + assert "count_write" in json_response["register_rows"][0] + + @pytest.mark.asyncio + async def test_registers_json_invalid_params(self, client, simulator): + """Test the /restapi/registers endpoint with invalid parameters.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Registers", + "range_start": "invalid", + "range_stop": 5, + } + + async with client.post(url, json=data) as resp: + # At the moment, errors are stored in the data. Only malformed + # requests or bad endpoints will return a non-200 status code. + assert resp.status == 200 + + json_response = await resp.json() + + assert "error" in json_response + assert json_response["error"] == "Invalid range parameters" + + @pytest.mark.asyncio + async def test_registers_json_non_existent_range(self, client, simulator): + """Test the /restapi/registers endpoint with a non-existent range.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Registers", + "range_start": 5, + "range_stop": 7, + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + # The simulator should respond with all of the ranges, but the ones that + # do not exist are marked as "invalid" + assert len(json_response["register_rows"]) == 3 + + assert json_response["register_rows"][0]["index"] == "5" + assert json_response["register_rows"][0]["type"] == "bits" + assert json_response["register_rows"][1]["index"] == "6" + assert json_response["register_rows"][1]["type"] == "invalid" + assert json_response["register_rows"][2]["index"] == "7" + assert json_response["register_rows"][2]["type"] == "bits" + + @pytest.mark.asyncio + async def test_registers_json_set_value(self, client, simulator): + """Test the /restapi/registers endpoint with a set value request.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Set", + "register": 16, + "value": 1234, + # The range parameters are her edue to the API not being properly + # formed (yet). They are equivalent of form fields that should not + # be present in a smark json request. + "range_start": 16, + "range_stop": 16, + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + + assert "result" in json_response + assert json_response["result"] == "ok" + + # Check that the value was set correctly + assert json_response["register_rows"][0]["index"] == "16" + assert json_response["register_rows"][0]["value"] == "1234" + + # Double check that the value was set correctly, not just the response + data2 = { + "submit": "Registers", + "range_start": 16, + "range_stop": 16, + } + async with client.post(url, json=data2) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + assert json_response["register_rows"][0]["index"] == "16" + assert json_response["register_rows"][0]["value"] == "1234" + + @pytest.mark.asyncio + async def test_registers_json_set_invalid_value(self, client, simulator): + """Test the /restapi/registers endpoint with an invalid set value request.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/registers" + data = { + "submit": "Set", + "register": 16, + "value": "invalid", + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + + assert "error" in json_response + # Do not check for error content. It is currently + # unhandled, so it is not guaranteed to be consistent. + + @pytest.mark.parametrize("response_type", [ + http_server.RESPONSE_NORMAL, + http_server.RESPONSE_ERROR, + http_server.RESPONSE_EMPTY, + http_server.RESPONSE_JUNK + ]) + @pytest.mark.parametrize("call", [ + ("split_delay", 1), + ("response_cr_pct", 1), + ("response_delay", 1), + ("response_error", 1), + ("response_junk_datalen", 1), + ("response_clear_after", 1), + ]) + @pytest.mark.asyncio + async def test_calls_json_simulate(self, client, simulator, response_type, call): + """ + Test the /restapi/calls endpoint to make sure simulations are set without errors. + + Some have extra parameters, others don't + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/calls" + + # The default arguments which must be present in the request + data = { + "submit": "Simulate", + "response_type": response_type, + "response_split": "nomatter", + "split_delay": 0, + "response_cr": "nomatter", + "response_cr_pct": 0, + "response_delay": 0, + "response_error": 0, + "response_junk_datalen": 0, + "response_clear_after": 0, + } + + # Change the value of one call based on the parameter + data[call[0]] = call[1] + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + + @pytest.mark.asyncio + async def test_calls_json_simulate_reset_no_simulation(self, client, simulator): + """ + Test the /restapi/calls endpoint with a reset request. + + Just make sure that there will be no error when resetting without triggering + a simulation. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/calls" + + data = { + "submit": "Reset", + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + @pytest.mark.asyncio + async def test_calls_json_simulate_reset_with_simulation(self, client, simulator): + """Test the /restapi/calls endpoint with a reset request after a simulation.""" + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/calls" + + data = { + "submit": "Simulate", + "response_type": http_server.RESPONSE_EMPTY, + "response_split": "nomatter", + "split_delay": 0, + "response_cr": "nomatter", + "response_cr_pct": 0, + "response_delay": 100, + "response_error": 0, + "response_junk_datalen": 0, + "response_clear_after": 0, + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + data = { + "submit": "Reset", + } + + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["result"] == "ok" + + @pytest.mark.asyncio + async def test_log_json_download(self, client, simulator): + """ + Test the /restapi/log endpoint with a download request. + + This test is just a placeholder at the moment to make sure the endpoint + is reachable. The actual functionality is not implemented. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/log" + data = { + "submit": "Download", + } + + # The call is undefined. Just make sure it returns 200 + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["error"] == "log endpoint not implemented" + + @pytest.mark.asyncio + async def test_log_json_monitor(self, client, simulator): + """ + Test the /restapi/log endpoint with a monitor request. + + This test is just a placeholder at the moment to make sure the endpoint + is reachable. The actual functionality is not implemented. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/log" + data = { + "submit": "Monitor", + } + + # The call is undefined. Just make sure it returns 200 + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["error"] == "log endpoint not implemented" + + @pytest.mark.asyncio + async def test_log_json_clear(self, client, simulator): + """ + Test the /restapi/log endpoint with a clear request. + + This test is just a placeholder at the moment to make sure the endpoint + is reachable. The actual functionality is not implemented. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/log" + data = { + "submit": "Clear", + } + + # The call is undefined. Just make sure it returns 200 + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["error"] == "log endpoint not implemented" + + @pytest.mark.asyncio + async def test_server_json_restart(self, client, simulator): + """ + Test the /restapi/server endpoint with a restart request. + + This test is just a placeholder at the moment to make sure the endpoint + is reachable. The actual functionality is not implemented. + """ + url = f"http://{simulator.http_host}:{simulator.http_port}/restapi/server" + data = { + "submit": "Restart", + } + + # The call is undefined. Just make sure it returns 200 + async with client.post(url, json=data) as resp: + assert resp.status == 200 + + json_response = await resp.json() + assert json_response["error"] == "server endpoint not implemented" From cdcfc6f96a4a1a2344939388b995369a1fb2c6f2 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 27 Aug 2024 10:55:29 +0200 Subject: [PATCH 15/17] Slave=0 will return first response, used to identify device address. (#2298) --- pymodbus/transaction.py | 140 +++++++++++++-------------- test/framers/test_tbc_transaction.py | 15 --- test/test_transaction.py | 14 --- 3 files changed, 68 insertions(+), 101 deletions(-) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index ce8048604..a7fad3d7b 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -214,82 +214,78 @@ def execute(self, request: ModbusRequest): # noqa: C901 ): Log.debug("Clearing current Frame: - {}", _buffer) self.client.framer.resetFrame() - if broadcast := not request.slave_id: - self._transact(request, None, broadcast=True) - response = b"Broadcast write sent - no response expected" + broadcast = not request.slave_id + expected_response_length = None + if not isinstance(self.client.framer, ModbusSocketFramer): + if hasattr(request, "get_response_pdu_size"): + response_pdu_size = request.get_response_pdu_size() + if isinstance(self.client.framer, ModbusAsciiFramer): + response_pdu_size *= 2 + if response_pdu_size: + expected_response_length = ( + self._calculate_response_length(response_pdu_size) + ) + if ( # pylint: disable=simplifiable-if-statement + request.slave_id in self._no_response_devices + ): + full = True else: - expected_response_length = None - if not isinstance(self.client.framer, ModbusSocketFramer): - if hasattr(request, "get_response_pdu_size"): - response_pdu_size = request.get_response_pdu_size() - if isinstance(self.client.framer, ModbusAsciiFramer): - response_pdu_size *= 2 - if response_pdu_size: - expected_response_length = ( - self._calculate_response_length(response_pdu_size) - ) - if ( # pylint: disable=simplifiable-if-statement - request.slave_id in self._no_response_devices - ): - full = True - else: - full = False - is_udp = False - if self.client.comm_params.comm_type == CommType.UDP: - is_udp = True - full = True - if not expected_response_length: - expected_response_length = 1024 - response, last_exception = self._transact( - request, - expected_response_length, - full=full, - broadcast=broadcast, + full = False + is_udp = False + if self.client.comm_params.comm_type == CommType.UDP: + is_udp = True + full = True + if not expected_response_length: + expected_response_length = 1024 + response, last_exception = self._transact( + request, + expected_response_length, + full=full, + broadcast=broadcast, + ) + while retries > 0: + valid_response = self._validate_response( + request, response, expected_response_length, + is_udp=is_udp ) - while retries > 0: - valid_response = self._validate_response( - request, response, expected_response_length, - is_udp=is_udp - ) - if valid_response: - if ( - request.slave_id in self._no_response_devices - and response - ): - self._no_response_devices.remove(request.slave_id) - Log.debug("Got response!!!") - break - if not response: - if request.slave_id not in self._no_response_devices: - self._no_response_devices.append(request.slave_id) - # No response received and retries not enabled + if valid_response: + if ( + request.slave_id in self._no_response_devices + and response + ): + self._no_response_devices.remove(request.slave_id) + Log.debug("Got response!!!") break - self.client.framer.processIncomingPacket( - response, - self.addTransaction, - request.slave_id, - tid=request.transaction_id, - ) - if not (response := self.getTransaction(request.transaction_id)): - if len(self.transactions): - response = self.getTransaction(tid=0) - else: - last_exception = last_exception or ( - "No Response received from the remote slave" - "/Unable to decode response" - ) - response = ModbusIOException( - last_exception, request.function_code # type: ignore[assignment] - ) - self.client.close() - if hasattr(self.client, "state"): - Log.debug( - "Changing transaction state from " - '"PROCESSING REPLY" to ' - '"TRANSACTION_COMPLETE"' + if not response: + if request.slave_id not in self._no_response_devices: + self._no_response_devices.append(request.slave_id) + # No response received and retries not enabled + break + self.client.framer.processIncomingPacket( + response, + self.addTransaction, + request.slave_id, + tid=request.transaction_id, + ) + if not (response := self.getTransaction(request.transaction_id)): + if len(self.transactions): + response = self.getTransaction(tid=0) + else: + last_exception = last_exception or ( + "No Response received from the remote slave" + "/Unable to decode response" ) - self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE - + response = ModbusIOException( + last_exception, request.function_code + ) + self.client.close() + if hasattr(self.client, "state"): + Log.debug( + "Changing transaction state from " + '"PROCESSING REPLY" to ' + '"TRANSACTION_COMPLETE"' + ) + self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE return response except ModbusIOException as exc: # Handle decode errors in processIncomingPacket method diff --git a/test/framers/test_tbc_transaction.py b/test/framers/test_tbc_transaction.py index eb918727a..32d64495d 100755 --- a/test/framers/test_tbc_transaction.py +++ b/test/framers/test_tbc_transaction.py @@ -160,21 +160,6 @@ def test_execute(self): ) assert isinstance(trans.execute(request), ModbusIOException) - # Broadcast - request.slave_id = 0 - response = trans.execute(request) - assert response == b"Broadcast write sent - no response expected" - - # Broadcast w/ Local echo - client.comm_params.handle_local_echo = True - recv = mock.MagicMock(return_value=b"deadbeef") - trans._recv = recv # pylint: disable=protected-access - request.slave_id = 0 - response = trans.execute(request) - assert response == b"Broadcast write sent - no response expected" - recv.assert_called_once_with(8, False) - client.comm_params.handle_local_echo = False - def test_transaction_manager_tid(self): """Test the transaction manager TID.""" for tid in range(1, self._manager.getNextTID() + 10): diff --git a/test/test_transaction.py b/test/test_transaction.py index e7cdc23f2..81c829cb6 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -161,20 +161,6 @@ def test_execute(self, mock_get_transaction, mock_recv): ) assert isinstance(trans.execute(request), ModbusIOException) - # Broadcast - request.slave_id = 0 - response = trans.execute(request) - assert response == b"Broadcast write sent - no response expected" - - # Broadcast w/ Local echo - client.comm_params.handle_local_echo = True - mock_recv.reset_mock(return_value=b"deadbeef") - request.slave_id = 0 - response = trans.execute(request) - assert response == b"Broadcast write sent - no response expected" - mock_recv.assert_called_once_with(8, False) - client.comm_params.handle_local_echo = False - def test_transaction_manager_tid(self): """Test the transaction manager TID.""" for tid in range(1, self._manager.getNextTID() + 10): From 0049eaf246e192ee52754b026ec9e8a21ae19138 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 27 Aug 2024 11:59:01 +0200 Subject: [PATCH 16/17] Better error message, when pyserial is missing. (#2299) --- pymodbus/client/serial.py | 18 +++++++++--------- pymodbus/transport/serialtransport.py | 6 ++++++ test/sub_client/test_client.py | 10 ---------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 833c7fd41..c48e9f43b 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -1,10 +1,11 @@ """Modbus client async serial communication.""" from __future__ import annotations +import contextlib +import sys import time from collections.abc import Callable from functools import partial -from typing import TYPE_CHECKING from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException @@ -14,15 +15,9 @@ from pymodbus.utilities import ModbusTransactionState -try: +with contextlib.suppress(ImportError): import serial - PYSERIAL_MISSING = False -except ImportError: - PYSERIAL_MISSING = True - if TYPE_CHECKING: # always False at runtime - # type checkers do not understand the Raise RuntimeError in __init__() - import serial class AsyncModbusSerialClient(ModbusBaseClient): """**AsyncModbusSerialClient**. @@ -82,7 +77,7 @@ def __init__( # pylint: disable=too-many-arguments on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus Serial Client.""" - if PYSERIAL_MISSING: + if "serial" not in sys.modules: raise RuntimeError( "Serial client requires pyserial " 'Please install with "pip install pyserial" and try again.' @@ -191,6 +186,11 @@ def __init__( # pylint: disable=too-many-arguments framer, retries, ) + if "serial" not in sys.modules: + raise RuntimeError( + "Serial client requires pyserial " + 'Please install with "pip install pyserial" and try again.' + ) self.socket: serial.Serial | None = None self.last_frame_end = None self._t0 = float(1 + bytesize + stopbits) / baudrate diff --git a/pymodbus/transport/serialtransport.py b/pymodbus/transport/serialtransport.py index 111116b1b..11759c2ac 100644 --- a/pymodbus/transport/serialtransport.py +++ b/pymodbus/transport/serialtransport.py @@ -4,6 +4,7 @@ import asyncio import contextlib import os +import sys with contextlib.suppress(ImportError): @@ -18,6 +19,11 @@ class SerialTransport(asyncio.Transport): def __init__(self, loop, protocol, url, baudrate, bytesize, parity, stopbits, timeout) -> None: """Initialize.""" super().__init__() + if "serial" not in sys.modules: + raise RuntimeError( + "Serial client requires pyserial " + 'Please install with "pip install pyserial" and try again.' + ) self.async_loop = loop self.intern_protocol: asyncio.BaseProtocol = protocol self.sync_serial = serial.serial_for_url(url, exclusive=True, diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index f4b6adc2b..b59d9149b 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -245,16 +245,6 @@ async def test_client_instanciate( with pytest.raises(ConnectionException): client.execute(ModbusRequest(0, 0, 0, False)) -async def test_serial_not_installed(): - """Try to instantiate clients.""" - with mock.patch( - "pymodbus.client.serial.PYSERIAL_MISSING" - ) as _pyserial_missing: - _pyserial_missing = True - with pytest.raises(RuntimeError): - lib_client.AsyncModbusSerialClient("/dev/tty") - - async def test_client_modbusbaseclient(): """Test modbus base client class.""" client = ModbusBaseClient( From 50f500d65583c4bea67db227341c086daf79ab5e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 27 Aug 2024 11:59:38 +0200 Subject: [PATCH 17/17] Prepare v3.7.1 (#2300) --- .gitignore | 4 ++-- AUTHORS.rst | 1 + CHANGELOG.rst | 18 ++++++++++++++++++ build/.gitkeep | 0 build/README | 5 ----- dist/.gitkeep | 0 doc/source/_static/examples.tgz | Bin 43223 -> 42947 bytes doc/source/_static/examples.zip | Bin 38433 -> 38438 bytes pymodbus/__init__.py | 2 +- 9 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 build/.gitkeep delete mode 100644 build/README create mode 100644 dist/.gitkeep diff --git a/.gitignore b/.gitignore index c2a1d0c33..c63412f59 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,9 @@ __pycache__/ .venv .vscode .vscode/ -build/ prof/ -/dist/ +dist/pymodbus* +build/html /pymodbus.egg-info/ venv downloaded_files/ diff --git a/AUTHORS.rst b/AUTHORS.rst index 78fad7682..8903a4d19 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -33,6 +33,7 @@ Thanks to - Dominique Martinet - Dries - duc996 +- efdx - Esco441-91 - Farzad Panahi - Fredo70 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1cae14415..c25aa7621 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,24 @@ helps make pymodbus a better product. :ref:`Authors`: contains a complete list of volunteers have contributed to each major version. +Version 3.7.1 +------------- +* Better error message, when pyserial is missing. +* Slave=0 will return first response, used to identify device address. (#2298) +* Feature/add simulator api skeleton (#2274) +* Correct max. read size for registers. (#2295) +* Ruff complains, due to upgrade. (#2296) +* Properly process 'slaves' argument (#2292) +* Update repl requirement to >= 2.0.4 (#2291) +* Fix aiohttp < 3.9.0 (#2289) +* Simplify framer test setup (#2290) +* Clean up ModbusControlBlock (#2288) +* example docstrings diag_message -> pdu.diag_message (#2286) +* Explain version schema (#2284) +* Add more testing for WriteRegisters. (#2280) +* Proof for issue 2273. (#2277) +* Update simulator tests. (#2276) + Version 3.7.0 ------------- diff --git a/build/.gitkeep b/build/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/build/README b/build/README deleted file mode 100644 index 0d8c54515..000000000 --- a/build/README +++ /dev/null @@ -1,5 +0,0 @@ -This is the place, where temp files are kept -- html (documention) -- cov (coverage) -- prof (profiling) - diff --git a/dist/.gitkeep b/dist/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 65294ff1e53b22de591aec1515db7f75ce6be235..5b4f1df953f7f757480f9076770d04048cf3b1cf 100644 GIT binary patch literal 42947 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℞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

@ZY!>N8lNg(SPWOV&WZYeBvV`P9W*$vE zEMBS<#0F5N#BcmfnaiYSNxwr1z{jb*R^`_T-zkdNxSj`<{LHuv{ z-l8n0tyW`eyVb(@-&?KD`u_JQpTGIXzkTxSCr`fh``+6l?++52@Y$2UgMUr<_XGUH z-~X{(lY7UjC_n9KU($D_^&R2A}9tE{&9DNKX{-hr~`PHwV{Lg>=?|=X2 z!59Cwf=RAFMW5XMf9GF)11wMwKVKOgSh)Wib$kCe>#eQU6Ypv@Q=I?#%=dq*?v2xM z9CRDo&GvSu(Q3A8J6nyddV6Q5v(ef1ULU;JJAC=g!FT($D?d%+nrpju_kMq{_f`Cd z_T>-Le?9nPveEv^JAz8D|2VIbvB%ef;h*S}+yBO{f4-tV7VUrBFPZbdy$$=nv!edz zem?X4pO^nfBYzb_f8(<#|4X~m%EW)}03}-Ue~N=+EAM-;X5zR%b*d#eDm4zuF266IXUeCsKS?-Ec1DpA^*;pZ*D+j$m)9&T z&hV(h?b|t<#T4ztEAtbKXPVB-{xH?EKvwD9Cgf}xQM!VqAvPHs66M;+pBU0E60dja zP5lI8XphI}#!MhaDHf{u15}>q+xd`S+0BF35TwCYh-PWGVM@}37F`EQn>$reEEvfA zK)9pIox1^uD)ne%cD*JG8=_cP1t?Cjs>+il#DIpAm0|4kGf!NrV=_ct`Uxdq(k7-=AQ96IeEXWC3p!B|#zR_9p|B zh`o!Wb65~1A$D}HLnR=gGosp|qowu|M__SAP6aZJTjf@>Uf-#D4MROZ&%Vk(Yt$Xj zwj9s49nTznG#q_wwX0q;^K5%3|7@q8d)8>=>TWk0t>xM?pBXxWk^kd$kEGZ=Y4pMN!eH_sby-|N(B+?KSN3}2nM{#_hYA( z9F~svkB)aeMVe{&<(upzZY$mmlrc9zAxt&_D7jL|zlD>T?V=zgs~3KPx3F>yGcBnk zFr~_}gP%moR4OSP|7u5m221q8R1V=hvh8JJ_mPGfs0va9iZD|yeY1D?ic`B)If+_& z;ZFdigui<7MQMp7D|}@AGOwsP{{L`)@71^awKz#v*2e<-pL)BKvHwB-zqRK7ALG-V z^VxXGI5ESx4F@*YTicjhbL03TNL0?mgu)Rdv$HYgcOQ6X*Jw!@Q*vp=jCyvJ4XpC! z?J*pm{n2a?sMK8kERDvLK9cSm$t)yQ&jek+fk{U?aXteM3>bl6umRT+Je}bN@;z#u zp_K2Ph38)B_?x{qzw`e1_I(MjE~v%6MswfGiwMZ&$G|HMfR{r~N{y)YOQZm~{U2OS zM?mjZDFK+j|6$kHJDL0cRu|9O-TGOgcm(pItZ`sxk5KqBIP$%Lk{KMe=KXb=3_ zBuJlg!RMSBjct4r&fH2DO}HD*(pikhs!F&7`%~PTr941KkCRWLL|N-yV5~OzD-Fg| zylrl%XgFj-rsKG&M_dAvsn&#nhA8pP@wczpFoTQC;*k-yPRzl4RU>{wS3iD7tOall zhkNf1UiFS%?!DUUy+3@7zBu#2RDw$XR}HZYo%|R~M4~pfg`<%TnA1EYr^H>C6Kfhp zn`3`+y%|!5*M6{x>>Uy6L|M&+oUN7<8OF0dE(9*~MKl_~H8x4Li9V3wLmaV3lhLu< z4EaDW9PD~Bqt+;xoTnH1_Y95h%mNsIqlf-&`catl*h~ zjBqN1ieg-73uwRDgdg zrDNiW2ivP(QQ;Gjuo6{LR!=NbZge)GoPgA!2rr~s=ha~%N02hen4BO}``$n$f+Y%~bO$bq^`xKCmg%sqFeNvEVH+4OT1Um2Lq zwAa7z<2V>5*>pO*y~sSOrfTOZWgpm=Z*~U~DKbus02=>y8b{D{dTkAIAhTu=$`gfz zeRrA-jc>$7!y5KPIkQPmHO1LcXaiHLc^V=v^_0O3eY~e-hd_dREts5)({DB*9be&5 zPwYiFn7(h-L6=5s?RwLM zmmBy$h|I4jM?G4c$|_=8S8nM&5*Pm*8Z^LkTe8iYrCIsAw@1gut4dyAE&d9KpkPdL z7J5Cyp?ZkQ1M+>ZcwcnAX1(qb<0wWeAK4oK+*NJCk9>4)Y`vTBE?;n*oa=IMW+%}k zU+7{GC%V)H8OJ2~V#BE}G@KU2{gy@K<}`aBz>XJT0Zijg=1}s4$}#{Q!ezH?HLlDu zGu-3m1oI&eP>3H#vt(0{TTV5lxF9r;#)7?J7>owxX&go|f9Eq<>~2LXv}GvujXNG5 z?;7(a>VYp$XuA>%Vz!GR5E_Ow0zSIy7a2qHEj;#j~myd;7#_ z_f%5`k#ie<^*CUNTtgirtYmY{z}MwwQ93r2ZUao%4KHFdgnx=z6q zB;PI)=aVvXdS3mi-l|l+GI53Qt5L6<=I|iT(b**C*|Y3W?7eI=(A_-bjxy0SOBSMb z8yPwzZ#RyXelK3-voI|K3z)qD3xJFP3*8Hzj)zughmzg8$m)8iE(Kg@)nL_^I0qnJ zT$3LLyv(M$P?wGFNsc1pj*tCI_EtCdcir68*;;gcleG|}8KR-TDwUTQlL|NS-a(~d$Ik`~boT%7C~04ox_hnQb?Dr$Dhq z(H4lN-10P!rJsi1GTVSaM+6EN9^sYEydg;kKnAB@itHnfeGx)=C#Gd{hg#HCF2BND zSnb&d$M4~q^!|&H_O8QZv-Hbkrv04m<*XE4DBctflzsIO%xvnk8`|8t(q4tDob2;W z&@zX8#F`8EpETr*Gg|ohQN=|42sG7CttFYv4sS_Ru-vH5nJ^Bn2fUuFxd*aL`zc#I z``ssK^fPd|#s*{wFp(6FXAKqg#t@(kbF`%waViKPP5Tm1i3g&_2$&(G$t)-`v)YgF zd=c}B0sa*LsyE~=4H|b8!d#=J8U~mnj~9Re9|}d2XswA&E<6!Qz#TIhxc?e$yo5Ru!;$JD95|{wXJ@2V-dO_x!$e zf2J$T1?xW)JQ5Pdhu*b^BU)rv!F+rt+U(y? zc$AVGnxP^P83EcB+Cx!q@N)JQZy)~UCqTH!Znb8TM_P~>7MzDs;cz(y9p;|#5%ZvJ zV7$jRY=g^Y%}q@TCdA>g_wg0UkC$K_xHhE^9uArizy}mmpjo{KOmm7@zEeA>e}-pdv73fp%Z^4{ubf zNQT1W0s6??1lsFm=Ho;2MJY2`fX2w}%Fmv3$V^Gh{0hYn-X>DC(>}YElyS4v@$ewe zWKTusj}NFyq4^t(cPOU7VZwr1U1Ucp9A1dNb8-zzVLaP`Olcc(XQk+%uOc576gC}` zc#Lbv{P=oaLHViIU)S)-OEAur>l)DK7Afgr|`rAMB5Ap zA{T`S|B?nec=A5sA!xD95#v|ybG)c8=k+U~{-^HLCSGG5-L%I1q8;69T=eY7WSxFr z$?~U$NHhuLNx+wToP25mk|&qL&C5KJih(!0f01q-9SEd`R~yE zf3dj`?<7z!BOG37*6D#_;C=Eu4)_1k>znoa_W$c!np&Rwe|;*~-xL=kqWkji{fDVc zEr$I)|0T)(KKi%db{C^C4<^SboDzj_4~~9CVX=q6VVF2Xh9w>edL}~Bu*@Sv&tym* zR(KTXSrRG=D?Li|EDe=}OFgCNSr$@-%RFT`Cc-)}O8zQFDSV~h7BiW9)G5&!%vXFC zqs2HjhP`$&i7kl7uq3W6N>}VZywu&_)9@ku%%#OM3O97g@%BZdA@G-KC=vi-7GRkW z*%%_T;28oQ9t?iv+@Z?gkE#QX%EU9`o~S>M@Wt5{il7$qVW3?wkBGuOVoF4bDdKlT z6#1&iLr{`eF%L;e!CgYBDcP&IM+$!B;8#YKQws1ar;5N^1*HV{BC3Ka24PC71pHM@ zm4bT-r2_X-stnv!DY3RPQ|g`z@k^MtqeDCyF33B{6RlH0Gir#n+(Oj~1x|Rqkg*3K z$S*q76N;V<8Ch1ScWFf#5*>4#k--n3sOQDmN<&Bp0{kz94e$;_BKRVfLkmEE;dy_F z^8U&;AGQepBDlXqv}jQj0R#p@NiGzdDJ~>n5GoT472u`cW^>1}6s{Gg{ZDb8C~AYN z%nveP?T?UcovYjv7sHiS15O%<`>r-NHeL<;!cjVPRV&R%$9)knb@(#!aiEY+!0$2= z=1}QO5$_;qS_#Tb!@6e_ zXsj{YuZ@NKe)D+!Y$_wmI-jI#_K_LqmQoEeigkh8Eo@JT)f`_UUQ_PYz5sqocB|^Z zPtoqNlS{<5tY)_o{;Bx>*~@RpHuQGzT)Zn&zFxXqx=X)~!BiKJ2 zh4F{kBLYzv=s8h(?o&UEG+n`3K=O_ry@%by=?d5iu*4d=3ig7OJwr8JUD$5u#X0za zlxa2vraDj8?5{JCt@E#;IsKBmT#!fuZh+=)`zxb z*LF+GU;pCr7gwmaf^P)ZPQMvlOWr59N^fk)Zv5PnVfvqbIS-wsPeB^1@cUQ?s8|7& z(WfDu5D;9QJ`4Xw>^q-|^qB+b2%C{VDRqI#Lk!~Y zM*#a6sc4A_2psl{aa1lFpD`s|&6-tx3xQbvX)EmF12wsyWMcv(>(WYiG%F7IB0w5p zvOu&8$QXPECS}MS>GSY*0R~H$gfbE)ZVZxzhDYA9gUOqFh+%Go;)gVeNTDy2O!+B?6^(c4c-Z^p8k4tHMuT%Pua$hK40d6QUoIX zmqd{C4<~pyFn~&i2kk}LJxN@}y{{;I|AeVXpD>kb?hBw0Q?hsD+2Q?dwoO6%acu?Kr=~Ei znPY`vhxZA7!26jO1is-?952m=r4-tt^lu30icrZzYkQ2_*1=jMWCl6S5i$ctg*#Y> z9OP$_pac64AF*f3ywg5!a6ASrjqz#dJ**XH2%mR4no+W*0^;{w?w=rt0@#F;WUak;U&@2 z>5Fl`bw%hKp>ZR`74hK3r%FSB^DT;kFMaYf7R6Bf0YVUX8>|B(I?G{LZg7)WBo446 zeS&;vN(;lXyo8rCHlr$xH&ZT98Y27EOy$8T-y|1FEB3m?EAnGr(aV4{-;^v$W~vIm zxFdQ^MSwUrC2nxX(NhuwIz0iz{szPnv`ThN#(W_-6d`3~a4&5Fzw}l3hb#)wj>jic z8SzBSi^gh3>Ra%66JQ9-rhIYlG}Q2+h^v({3Ye7T0vQQ&xDb62hLS)or&AdT;|!b; z)6pcdhhzmDG|uZWDCHE4b0AK~z?;}#W5^7ks<>Npr)WoYdQ)}!fl{^0{C&6hPB9a< z>fcmd$qPNRsXDVdFC4l~zWv4f_~r%Erpok4N)(qo5=+VyOXOY&R#tVlalB)jM8|E~1%iubnouKerr@0DKOkX`;M_=!Ox`^&54Ruz#a+H<3N7K5YO5BfVd z59b7SEA}!_NuUgLQ*u+fND7VfMS@WafYd=yBJ?C$l%SC!UX%vJxw-PS3k0@E0+o`t+SyTwqai~> z3CcdG6`^54H^4XKzJ>*&J|o7XM%^#NOU9AFkgh6dLlpk&@RK&S{VMO)-l<(_TeYvB zx?Q_f*0Ln|NP?-#Z@2!NS62GJ_N6^BR;=C=6LQ6#1XEXmMw2TJX`}yhL>uLlJbGeL zaF)uCh4?5@7)wY|<#9^Ec}q|&B|-175-Z}T0@8b&^joyi_`q$>y=eMeEEOqQw)ZFd9)4fx1ezW5IR>j37 z+iqp;PUX4H%5yuFS2ioJtohbQwkoeL*>}rp?|$aaXTIrOJO7Q~X1Q)dsoO2BI7Bx2 zEBq)rK%<5I7WJ}#9w%h%%J#Al1yJI5xUmN3OCgg(oTGvR@)V!|Aw4Lko8|N{RxWzd zR0VW8UaAs)s#zN4)lN|RnyD@fE1+=!O)Ji6TKj2SD@{uogGEC|v@i~M6x92fIRHmL z8Og#pn)js(%(f?ONp2X>5L2F^Y%)3RqkTx$py5IiGo!%ykeL*?fkJ;CWBDEp*B1-v zAA%JNthg{U)?4tCKg7@wP*>e;ywkXPVXbw&_1&|#8@JRQOVVAw9I>T3yQ8Y#RMoFv z*ihAPsjh;VT~!I+3rQH?46Mtx&O?czK%n?$YQ1*r!nIA+wMV2F3KJv}CX|3_=F17| z)mvpIARv@hzW$}eM&tH%qT<$gUhRo_+&=F6$pD>kE%E0co8lahDao&CMfm zPFi2&#`vO`9T%dsJiC;`PTvZK;-PVDKb7wza41SJ6T4*pB!DD<9Pfwi!zcYS;8tju z;}%8W%6#k%?9&yO_fvSjB>bRMjf4r`X6cy?*%?ISfO9Yc&!861VA?Xc0L}PO0T~c( zc~3x?*^F|0d@eB&V&6!|$Em1ye4K_njy?`=C*e&CZ*aMgZpK)f0V6pfCWK~XIAJ2L zf!dF*gEv?XrcD*u8(Q7%2DewS?-PALIM;ap>_?cAY<_s0B+oxA5tGf26bkY)k7^X; z)kihOr19a|DpLQjsffJtsIG{tdRQkV6%W-?vhtz2h-`RNry$QV`M}h*<#Z4}-vGCa zDra?RU`!_@`k^iGZ7J_9H0dBqOs0$%8iPL*$h?v%;fK$K7;UaLKd`~KfHti_7XwIB znIZ@B-A4OXGU`ItaaxLs$-Q7*-x!m#qOcVo%U)#q?FOB|m_t91yTG0p$MNrDb$f&i zm%mR`en4FKfVlJlq5pt5^8wNPV;P3Get>B{QcjBSvl~^He~iJ0pX^m)vhs%*j?2GK z$Y2`s$1ymPUp;*T0mmjd+52Gc_aUQmNT3v{YEq7b>;0V+eFg?qUftVztXc&c7B_<@Synk zuB>jiSoII3r*>6k9~DVTB)9B9Kqia6>iQMeHc`PvzpzbQd_c&TPQBWhm&9(F`c~J2 zvZ~*Vt>{-3Z+vE}x_)hPv$|*`1_RZCy)sBtI%iFRm z4`k)H?c1_r4`jtlGw;c2_oN`^o($#WE_Zm25|pEKkpx4W5XyB7vKaD<c z2=C)P)4b=O-e#YX+J$Z(LdK0u%}k69tJ}uH}Tp2kB!IG{y2rm9Hu1%jh z5{q3c$tZHj{EQU{vhS>)pOnS1&nWpI??7FKT1SR}9yv$_%&7JQ#b-n`oe_iL^L1yq z{Lknr@CJoVb};V|r8Sa=*a^~#FBdHlOTIk~cKpPKtY)|5#A@01N>1;I$l}`F%40i~ z=Qk_Qug2Gg?w{JIJik@>;%(7K3asMTihkvl?eh8us%oe%Z)l7gCdY=c>$|5mPV{W6 zoVzN`s(Dj&ZbPNnRn_b&%QB@WAE-}koNV7xTQ-!IA6C$}%(l82Txxmpgj4D|nbjS?D$mQJOF|Z`e z=`l<{%d$tuFlLbx*}ln{0`9T;J?uN4c6k+JwM!_WBkl4>1c4Ke6c}Fm0ao=xO#fr> zc=-dY@dK=RPl6R4U%IfPIJT)cw$iz!ICo3Bi%EAd)h4F8U48e3J1=ZwCm%{G@e2=| zG`JPtv*Q?{TuOgWTniM>U$yUvF`|wZ5KsRVUWe*`aQOh9bUQwkJ?i#jDT7Dh|Fh@6 z>RX%jt%f!O-2c^PXnL;yeJa=U^Iw0duS3Uw?kw0(#0QQX|IGQXyz$@K($@O?{MS$8 z;+_8r{u!EeEvAN+Hhr7Us59!Hpa1#`e;scBeHOF-k>`Kt3~jpo^S`0x`T3up%9XYM z>i^L;^~0il_TQ}S>CvCz0`^}~NaR6I^&zoGjD86Z@w7X^MVL=KQ&(6nUkPUmE=9Yh zRcLRx>{TphGVqj93f9tFge<*Nd`qvVLJm_WgqE;D=p}(rRc^y0_);`;Ss+fu%$35> z{jQ#eelKvWa^d&TN`2-Nr^1;dl<7ao*}KuG9vB?bLo_dYc5o{ciP(hSr@5+NLBKQx z!+$oVxpD=ffg)2Kct$uLryD8Xd}BPG8gjQcod%(k6A22r4VqfPNVsx^fz22X%xX^S zO!@|0tFb}Xpl>^!d&ME0z#s<7M5obfnsa7#HHUfvip1H=%LTAdVB`T`I2zx-c|T`w zXW|!Z;aJksc15Sw%w;+V89*`97^^@EZ~R7 zAw;4{t$3f+Gox^)VsT&Cwh({?XSa2uU9yjFJdMq=umdyY2#R*xPfvJ#Q;E>H59EQo z?9oCuw2?z=CTkaL+9q@l6crmp1!EoLH$v9d_vkcM2z>yCB|(W1O zkE1w1eoRlKPaHs#8n&nSc101}TaVOOA>sh?zN_4~SA!ukNJSFn@gB}>oo6>5!-Wjq zcK1^U6_eZd4y_L{IclmOzOMfp2q zmg6nT7Uj@>|5^pXcZZWvl6Rz>qnIMjInFOC7Kugil;|B9mjX7+Dzcml3AFvlgZODD1{3rrUQn$|$B8;xkvO3f7w#~eB9s$DArgo(Hw_ys&7z3V0TtwutdTf@@QZd9{egs^Nw}mZdWvXV+S!VNhZ)iGZ3xe~}Cx z;CxqOkf=1diy3udIuS`9KXAZ-x6m#f`(NPpzp6N0@v31FV zvNLOj_4vEf+hx5=lHH1vtHjFu>Pzbv*XuUaFTx#&%(}?Pw(Nynl^S~KS3ksX`L!kU zgKF*C`TO{G_0=WY?NeXt+!KS~e^8%W8@g}UR-3`MRp)k<<-3(9R*N<&E`LOd+1)xa ztWvWoUwLKi{Q9x=;tlnUec4o8T&wfgHM4T>l$D^}+?weL4>`)dXGNyrZpq5}Zk;t->B9Bnv2<7u< z0#MX!`h^2Y1->@GZ;V-5LC(%!ySA&Yx?6vzen)+NQ+<9*eQ`&5aYK3WfwCO#KHaFj z@}9C`kHnPKyUL4n4`!6# z1Clu<$*nU3eotmQ^?>hWl$=uVz9>2hjet^8#Z*aL_7q=~QYxzKslHHC+zf+U4rtdr~L_dzCkQqVc~~InbjdM zvlUUkn3oQqztbdAD70io27=-cFE>M?g}aOtjBq%?A$Gtn0j5tt02-))Abd#ftS|-} z1118iEtC@$$zI5#sgGMZM?O1>te71NNFG{VQ-231RZ*9gW%ZwQ zEu7(@!vs6IgK3_X;~(So*|MQ+d1M5t2R9&VKn@&?GY0%qTbhx9cj&-~Oo^H88Rl@a zj1mYK)jx#w}}BR z<(1O4@VkTCeQp#!mwfpxwtEsz_8@>}>9o#u* z*gOXWm_PmEAAa%O_+P*BomV!B1~!NRGz;+7YHJv0<SW?tvMMZvqrF<7_n)Lm~kVSiyd zZ8`Sy;-!43{g?Jpf!HTU06pCP+pO2=_xu00G#gr<+kZcei|_v%J$;b<_cyV8`)^p} z5ivGSALou`z*rnmXayF-e>rO^vIOU>!X(csEMcs|SPB z5|OH$iA%^S85zBBocl3*)H=9{0zE5*!nk zIfeYtxM*8DgqLKVxC3s3zEmN+8JeQ6vMtQ~#l->SNWii&CM6dTk=Z@-5F-)9&yc7? zIonGKyBR(_F*^(T2*hXLL7^JM16+i2a&B+<OtSx#0vB;1SjKTLr za5%>|Wyjx>onR^|2%yMfq<(AwMG+>9CvX&mB2<1*N^CCA(L6sknkUjVSTElaxN#LZ z^TXLD#)I@k`op1+o{SeM2k9xmld|ui<`HYho&?mhc`3eAdU~_;^lHnuuDyM2jsC`G zHcOk=JMIr{m0C7rmVYIr`AduN30LdVg*Els$hvP^_TtB(FK%PTzxx>UbvWjy=wN{8L$NydQyhcWnf|L^46sg? zhGK;@1QD)tM1&AGOCadVK1yO&I*^47?KjUtZtkb9*kRPgy@VUW(G5^n$pO^$$KLzr z|0uXEYu}Y!ULqK=YQ_3i_Z!`7qBnatWtRZ~PQ53)&X5OTu?{1-9c`SP|G!dSpCT^tl0$q})%{06i zih>apyU~#S<|SA-s9F5v7PdmY6@DYUW?3&=H*cM2y@eIh^vACcgVWIpZ^r#qKv*{) zr}QtwkD(F#S(Y?|Pjf9#`~1(axS#)*DQxx5bm59H(}8Aqvp=KK(=8a( z=RjLS#sR4O8P;L)nYT4U7oj_!ZBu)neK9}FZ`5Z?z|^xZtqrEu^K2W{_w0+wQEH5# z)O%vIqM5@ne?O9wCl&Bip<|Cq zo+A7`*)deY5220fF^+u*wJ2e={V}+G6+Z~8aG6tjzJ~JEfo zEHW!^^kl#K5-c1(j81}(qmN*VXQc4a7ojpz)}E4) zfu5O-Mq)k&koOC0>mpsMbYGp)r;e;P^rnx&O{70T`f6$UPRWVQk`pVjt&%fK;s+IV zE3K<7-@5Vkjjam(x^A<=uq1z=u34#CJ@>|mwF=}&sW#p(+k}3Uf3NO%r*bWI|H8ZG z|5(4JwQnQ%Mxdu%yK`-){LE(gnbnT1^2k-H@grerZxVo!67x7M zqC{?tf(kG3KUMjL-U-Ic;SbFFSW76h@Uo|ghf>g*fb6|!aWXf898;Q!NGiLW0M%CZ zJaUaI8u49*p%KnnjBJ-b&%@6(Xa|}&Tt~l0=ID+_(=IwXCyeC*6Qk(>TFL}hk5~l~ zdb`G;`UPl5jaAg_lwa5^zpy4=8(tsUD!;ZQ+mm3biWTE#=@|%2T*d(tS2MuG2UEZj z#%SxyPecP*oqmzvu~~u=3BzsYKv{YAHZm8wNP^Va380R#&wc~iXF-L3o_n~!7a8$Z zD68>!YX#)3i}(T_Q{qrEDQq8%<+fFT+hT~@;w~UXDG|?Lmz98^I)On!l@<_Gp^}uo zaxmi&V6<<-M4)KGGTQR%Yjd~F->bN^Qn6EeX|wjyR>h^w^6MK;TXNZ|- zfB~-ny!Rlw&cfLvh>O0jd zHCtt;mn09$POYBbD$^q5*xOSp^K0ZAU)(5d*pM|GvM8Y!L9+iU{#6Cvi8yBr&X-Y^ zpGZ2%lWGV^)KB!CgeT49@|P|P8k?8lfF!a5@@tB$acxH(C zVCSMbY*8w74g|#wT~rH2wmuYDheANUFlP}o*X2;a;RJC&joI?@n1*aw5M!1vS0ZUo z%Fh8X3<)JWK-!ZF!JIA17i9$)(Q=TtfYY!L2-%{X=iN_g<>@!wKPv{sH`M2h^m!Kg z0?F?gawi%JFbKkwPGV2Yied+{qTk2zS&{nM+Qe<$_sX>^#7>QNvqrmBt_8x*Pw5oQ z0QPyBOvIxw=2ZL}iNICf!oUZSxAgVtvc?hHJCt(zdexM`G~irzpd( z96yOa8>yaw5l3diFFcE${1f;cGKC=v6B5shX{eRc$d&`!lG$kmCldM;h2h&Ys>xP6Z+pAvy%Ze+AYGGQclMCZ-&1!7T zzHZx6UtN;EufDe4whiO$sw?hZxN~9U!j}3p{HqqIJN6CrnN{DnLT`uG+qTYM1L@uO zzdQBa#CIYa_Th~iBb(}xCFw^ZKot)RSMTfY``#V=>(77Z^IL|&k07Zmk$bZJs>S<4 z%EALRgwktUwQW^j+EUkp?6bZmHJh@UmFo9orw*AtZ(~nt8$di92#om}KXdwu01tnP zQ!N(rPBcKmqqFB19M9ed;!nFRA|dJXD%+0ps) zJS;)C!R`WT8Kg2+EjvU5q8B()=5?&|ppIq9cL#pdi?O~8LLJ-k;@=f0Ynv69HlRWT zZh&G}B&$_x=iWZCQQEX2Yx?O!F;;N{Ne(O3CPJ8x|2K@`i?U~H z32=8Zr|Iyv;FCFrnY>e6gie?fIqd@62BS(w|TC(ZifPrYTfD;3ALdpo&>u8HLYEXIh29u9L z=@;&(?tDZj$P*81NV4T&iHJP;NTwjII6R}nia)H8lQ$k7myqWls>(^lUK3VUe#^02 zUb`~3R`$lro8|SldL9zRWX;19C8Xk^rig5OSXWJ+-ZKhDRDg&e-Lo)R-J_}^@-$OA zv=g2O%P zXDk-;w8LWQx0vfk+td0fde~_-yW0K1x#?h_!<1}lH}}WvW^4PXvp<%!^p9DG`};d= z$u7HMC~fO^np--|x*?mzoa`KE?wXzq+k<0+x$yR6Uig61Y-Hm5lMaQqGZdfH4+N-D z=Ynm}+}G}R4Y!+}PD^LIVRFP29E%L6o$W27)ZOtu~#@>QB3dqae(gQoumh zAQzY%PTP8%<{*>vw9_&$3=+1Qdzpm&&h{oMH8x_H_H@`#xPp1%&40m_B${860bA0V zMD@2?&C^NWh;Clt>FCmV)1VREj-<7JtgAcfaZJy9UFLpUdwajx>i66F%vSKeKWYKb z?dC4$z|5p&M(1vAar;vPin!D1>>25tGBz!^$7U>j-lVB(G-#eN_szDoPFaR^lLl(R zt8X8e_a_I!Z3~lAeVz6pSC7RQi}wS;O)=fxZXIf$oEW9TQ%S$U+2Tlc`)t18e6QEu zJsEHJ={=qHKr-weYjF>>was+5rH3Ppx)Db*EI5piD8l4L^ zHn%l8jl-^1u(g#^M3VL0I)f1qq0MX_6d+%7FX#cAdB!l%M775|t&xRP!kqNm{X@ekXLL}J zn5qv&dQI^upQp8h@(%TNxhJO6(OGM-t*5=G$7$&79JUTcEzY>Do$@r#Ht8H4?)l*k zosOEaPsBY6+e}+@tgB-_Z0s|o^(_&7PtUxs#W86Mw&=r?_0v{kdaT9TZfzOqaQZ^N zIitrrIoCF3(#3T32~)(Vpq-AMQNsuw_5{82mh_a{JwHeFxEe>hWA+ZGX|#!2=t$Tb zeWSCXl)o)J**er_=^06OG$o_Pmd;4Of_8K!^-b;TRpB8BH|}&*9HhZbj0BHU%#$EY{llX7P6Y3H5>e; zy^fgwfui`y^EhJuZ`K={3g`dl_^+SJ#hd^81{}j+Kx(>eW8D02o9}nDn+xW98(^4j zOMACB>G!*5%znUBqaa?tWzaUJPex5ciG(fcR0JmntyYU}y1CKi911&5FyJs&mt z1{1bHy|t^u9#A+#me8cG%N?F+iG)4f=}s!ynOqp{=$NXXOFEsic25_W2KpmyF_)>u z)gPRRxre&b6UL!oQ(~gaU$2OGjD3N)Zwg8$X0r_n3FfYWp+@smzbR;nwRZP7lk-132qGg?14Es|1I|frTX z$uw&9#~12H?TyZfA=|)Me{ynO*VW!_rX5}W8Fx$ljHkJMx-HZ;H{UuPj}7}=9!sLN zabYIt56qht4xKgC($v;vHwK627d&ovyxVUw7^o@ROn=zj-K%$meDS%K-v0jXc7MOs zk{Vpl_l)*U&CCwjhq}y@3P)3(57ka1ro5q0XGdW}T_D+PGjfqHyp+4>j z8>aj9`o>N}I@+3=>@@ZHY__Snn5mu`Ragvzo_bwsa)Js610lzh#XU0H+u=|4b7}QS$ zQ>{Z|Op)8&Bfa)fQ)19)48-Q$hFNdx%vij)bBJ=!cg>hb+ZD!iZ)bC(r)|D4H#GImjIf%rgb ztW#kdn$?>ct#o4&7TIDmMq@$aw9`6m?THQa_&OScQ^_V{SIpHmH!d zYO)3$^RtwrQ8DfD&l?tI7NYe=og>jZX$durG&hG_9UTL-yQ8T)F%pUT0^2fGgb_ZnKSg zoztcfCp{SHr91ngV@-j{)_?;xa!0gH5i!QQC#=>`x2-EW+o%ssnEM6?;)zsvt}8N| z?gCQXH07@M&9_qRx`C<5Xnk}nK=&DSZN9NVpP|{M80ws8^Y^th%_ViTZ?w19*k`Rz zI1Ld)s4Zxl>UM;T9y&OnpYI&)at_)?OoMZYA)1=;`24-rQPX^%qE~Oy8QbRPJB_B+ z*kpQiVSaetwJ_3UY3&{8XtFnU>m7}H(}c$y?J(Qsrb2_J!I0nJvNyFZMB7KFyA`oO zV8CQG`9l8mP^xjJd2qOEGTi5k1Y8}VPUnoN%QoC+OhzKk(P*#H-fp(HHjTx7ao5O5 ztZR78GNSMrOjFG!2ko#o>gFeHEn`W*?!cspRNly(J!bxjiSQq07hy`=e&w8GGem%dQU(c^! PwCn!^fttgW05AgpW{t3H literal 43223 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

M2OGTuqiH)Y*dZ> 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 Q|8aJoMggxL_j_CaAGOdP5C8xG delta 9145 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 diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 91867a341..4134279cc 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -18,5 +18,5 @@ from pymodbus.pdu import ExceptionResponse -__version__ = "3.7.1dev" +__version__ = "3.7.1" __version_full__ = f"[pymodbus, version {__version__}]"