From bde95b18657d259a38a4ec45dd36f5f1d1df8014 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 6 Mar 2025 09:03:56 +0100 Subject: [PATCH] Change slave to device_id and slave= to device_id=. --- API_changes.rst | 10 +- doc/index.rst | 1 + doc/source/_static/examples.tgz | Bin 42359 -> 40233 bytes doc/source/_static/examples.zip | Bin 42617 -> 42583 bytes doc/source/client.rst | 38 +-- doc/source/library/datastore.rst | 2 +- .../library/simulator/calls_response.rst | 8 +- doc/source/library/simulator/config.rst | 6 +- doc/source/roadmap.rst | 3 +- doc/source/upgrade_40.rst | 47 ++++ examples/client_async.py | 4 +- examples/client_async_calls.py | 98 ++++---- examples/client_calls.py | 84 +++---- examples/client_performance.py | 4 +- examples/client_sync.py | 4 +- examples/contrib/drainage_sim.py | 16 +- examples/contrib/serial_forwarder.py | 22 +- examples/contrib/solar.py | 10 +- examples/custom_msg.py | 24 +- examples/datastore_simulator_share.py | 5 +- examples/helper.py | 4 +- examples/modbus_forwarder.py | 22 +- examples/package_test_tool.py | 16 +- examples/server_async.py | 42 ++-- examples/server_callback.py | 6 +- examples/server_hook.py | 4 +- examples/server_sync.py | 22 +- examples/server_updating.py | 22 +- examples/simple_async_client.py | 4 +- examples/simple_sync_client.py | 4 +- examples/simulator.py | 2 +- pymodbus/__init__.py | 4 +- pymodbus/client/base.py | 1 - pymodbus/client/mixin.py | 227 +++++++++--------- pymodbus/client/tcp.py | 4 +- pymodbus/constants.py | 10 - pymodbus/datastore/__init__.py | 8 +- pymodbus/datastore/context.py | 103 ++++---- pymodbus/datastore/remote.py | 16 +- pymodbus/datastore/simulator.py | 4 +- pymodbus/exceptions.py | 8 +- pymodbus/framer/rtu.py | 12 +- pymodbus/pdu/bit_message.py | 8 +- pymodbus/pdu/decoders.py | 12 +- pymodbus/{ => pdu}/device.py | 46 ++-- pymodbus/pdu/diag_message.py | 94 ++++---- pymodbus/{ => pdu}/events.py | 26 +- pymodbus/pdu/file_message.py | 8 +- pymodbus/pdu/mei_message.py | 6 +- pymodbus/pdu/other_message.py | 28 ++- pymodbus/pdu/pdu.py | 16 +- pymodbus/pdu/register_message.py | 12 +- pymodbus/server/base.py | 6 +- pymodbus/server/requesthandler.py | 22 +- pymodbus/server/server.py | 30 +-- pymodbus/server/simulator/http_server.py | 14 +- pymodbus/server/simulator/setup.json | 6 +- test/client/test_client.py | 12 +- test/client/test_client_faulty_response.py | 2 +- test/client/test_client_sync.py | 6 +- test/conftest.py | 6 +- test/examples/test_helper.py | 2 +- test/framer/test_multidrop.py | 2 +- test/not_updated/test_device.py | 20 +- test/not_updated/test_events.py | 16 +- test/not_updated/test_remote_datastore.py | 32 +-- test/pdu/test_decoders.py | 6 +- test/pdu/test_diag_messages.py | 40 +-- test/pdu/test_mei_messages.py | 2 +- test/pdu/test_other_messages.py | 20 +- test/pdu/test_pdu.py | 31 ++- test/server/test_server_asyncio.py | 25 +- test/server/test_server_context.py | 70 +++--- test/server/test_simulator.py | 10 +- 74 files changed, 802 insertions(+), 765 deletions(-) create mode 100644 doc/source/upgrade_40.rst rename pymodbus/{ => pdu}/device.py (94%) rename pymodbus/{ => pdu}/events.py (90%) diff --git a/API_changes.rst b/API_changes.rst index aa3630014..e1727fa10 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -2,13 +2,15 @@ API changes =========== Versions (X.Y.Z) where Z > 0 e.g. 3.0.1 do NOT have API changes! ------------------ -API changes 3.9.0 +API changes 4.0.0 ----------------- - Python 3.9 is reaching end of life, and no longer supported. Depending on the usage the code might still work -- Start*Server, custom_functions -> custom_pdu (handled by ModbusServer) -- payload removed (replaced by "convert_combined_to/from_registers") +- Start*Server, custom_functions is now custom_pdu (handled by ModbusServer) +- ModbusSlaveContext replaced by ModbusDeviceContext +- payload removed (replaced by "convert_to/from_registers") +- slave=, slaves= replaced by device_id=, device_ids= +- slave request names changed to device API changes 3.8.0 ----------------- diff --git a/doc/index.rst b/doc/index.rst index 92678510f..78c64cc6f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,6 +8,7 @@ Please select a topic in the left hand column. :caption: Contents: :hidden: + source/upgrade_40 source/api_changes source/client source/server diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 0c6bb7a14be9d3c93b2e1b8ba48840332d3d019a..3c1f328dfc7528d131337eee2276affdfea7fbbb 100644 GIT binary patch literal 40233 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 F0{{VWy72%2 literal 42359 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 E0Ftz^;{X5v diff --git a/doc/source/_static/examples.zip b/doc/source/_static/examples.zip index 14eac6857f96ea16ce061f9022138ff9071ebb16..dbe97d851074d2979f6e9ec33043570794c8f833 100644 GIT binary patch delta 14396 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 diff --git a/doc/source/client.rst b/doc/source/client.rst index 57adceaa3..354c32b8a 100644 --- a/doc/source/client.rst +++ b/doc/source/client.rst @@ -127,12 +127,12 @@ Synchronous example from pymodbus.client import ModbusTcpClient - client = ModbusTcpClient('MyDevice.lan') # Create client object - client.connect() # connect to device - client.write_coil(1, True, slave=1) # set information in device - result = client.read_coils(2, 3, slave=1) # get information from device - print(result.bits[0]) # use information - client.close() # Disconnect device + client = ModbusTcpClient('MyDevice.lan') # Create client object + client.connect() # connect to device + client.write_coil(1, True, device_id=1) # set information in device + result = client.read_coils(2, 3, device_id=1) # get information from device + print(result.bits[0]) # use information + client.close() # Disconnect device The line :mod:`client.connect()` connects to the device (or comm port). If this cannot connect successfully within the timeout it throws an exception. After this initial connection, further @@ -147,12 +147,12 @@ Asynchronous example from pymodbus.client import AsyncModbusTcpClient - client = AsyncModbusTcpClient('MyDevice.lan') # Create client object - await client.connect() # connect to device, reconnect automatically - await client.write_coil(1, True, slave=1) # set information in device - result = await client.read_coils(2, 3, slave=1) # get information from device - print(result.bits[0]) # use information - client.close() # Disconnect device + client = AsyncModbusTcpClient('MyDevice.lan') # Create client object + await client.connect() # connect to device, reconnect automatically + await client.write_coil(1, True, device_id=1) # set information in device + result = await client.read_coils(2, 3, device_id=1) # get information from device + print(result.bits[0]) # use information + client.close() # Disconnect device The line :mod:`client = AsyncModbusTcpClient('MyDevice.lan')` only creates the object; it does not activate anything. @@ -160,9 +160,9 @@ anything. The line :mod:`await client.connect()` connects to the device (or comm port), if this cannot connect successfully within the timeout it throws an exception. If connected successfully reconnecting later is handled automatically -The line :mod:`await client.write_coil(1, True, slave=1)` is an example of a write request, set address 1 to True on device 1 (slave). +The line :mod:`await client.write_coil(1, True, device_id=1)` is an example of a write request, set address 1 to True on device 1. -The line :mod:`result = await client.read_coils(2, 3, slave=1)` is an example of a read request, get the value of address 2, 3 and 4 (count = 3) from device 1 (slave). +The line :mod:`result = await client.read_coils(2, 3, device_id=1)` is an example of a read request, get the value of address 2, 3 and 4 (count = 3) from device 1. The last line :mod:`client.close()` closes the connection and render the object inactive. @@ -194,13 +194,13 @@ Client device addressing ------------------------ With **TCP**, **TLS** and **UDP**, the tcp/ip address of the physical device is defined when creating the object. -Logical devices represented by the device is addressed with the :mod:`slave=` parameter. +Logical devices represented by the device is addressed with the :mod:`device_id=` parameter. With **Serial**, the comm port is defined when creating the object. -The physical devices are addressed with the :mod:`slave=` parameter. +The physical devices are addressed with the :mod:`device_id=` parameter. -:mod:`slave=0` is defined as broadcast in the modbus standard, but pymodbus treats it as a normal device. -please note :mod:`slave=0` can only be used to address devices that truly have id=0 ! Using :mod:`slave=0` to +:mod:`device_id=0` is defined as broadcast in the modbus standard, but pymodbus treats it as a normal device. +please note :mod:`device_id=0` can only be used to address devices that truly have id=0 ! Using :mod:`device_id=0` to address a single device with id not 0 is against the protocol. If an application is expecting multiple responses to a broadcast request, it must call :mod:`client.execute` and deal with the responses. @@ -217,7 +217,7 @@ All simple request calls (mixin) return a unified result independent whether it The application should evaluate the result generically:: try: - rr = await client.read_coils(1, 1, slave=1) + rr = await client.read_coils(1, 1, device_id=1) except ModbusException as exc: _logger.error(f"ERROR: exception in pymodbus {exc}") raise exc diff --git a/doc/source/library/datastore.rst b/doc/source/library/datastore.rst index 3c1b570d7..01e1cdd1a 100644 --- a/doc/source/library/datastore.rst +++ b/doc/source/library/datastore.rst @@ -11,7 +11,7 @@ Datastore classes :members: :member-order: bysource -.. autoclass:: pymodbus.datastore.ModbusSlaveContext +.. autoclass:: pymodbus.datastore.ModbusDeviceContext :members: :member-order: bysource diff --git a/doc/source/library/simulator/calls_response.rst b/doc/source/library/simulator/calls_response.rst index 51b89ae17..cdca917e2 100644 --- a/doc/source/library/simulator/calls_response.rst +++ b/doc/source/library/simulator/calls_response.rst @@ -72,7 +72,7 @@ }, { "value": 17, - "text": "report_slave_id", + "text": "report_device_id", "selected": false }, { @@ -131,7 +131,7 @@ }, { "value": 4, - "text": "SLAVE_FAILURE", + "text": "DEVICE_FAILURE", "selected": false }, { @@ -141,7 +141,7 @@ }, { "value": 6, - "text": "SLAVE_BUSY", + "text": "DEVICE_BUSY", "selected": false }, { @@ -164,4 +164,4 @@ "call_rows": [], "foot": "not active", "result": "ok" - } \ No newline at end of file + } diff --git a/doc/source/library/simulator/config.rst b/doc/source/library/simulator/config.rst index 918ba3049..9cf695f04 100644 --- a/doc/source/library/simulator/config.rst +++ b/doc/source/library/simulator/config.rst @@ -87,7 +87,7 @@ Server configuration examples "comm": "tcp", "host": "0.0.0.0", "port": 5020, - "ignore_missing_slaves": false, + "ignore_missing_devices": false, "framer": "socket", "identity": { "VendorName": "pymodbus", @@ -123,7 +123,7 @@ Server configuration examples "port": 5020, "certfile": "certificates/pymodbus.crt", "keyfile": "certificates/pymodbus.key", - "ignore_missing_slaves": false, + "ignore_missing_devices": false, "framer": "tls", "identity": { "VendorName": "pymodbus", @@ -138,7 +138,7 @@ Server configuration examples "comm": "udp", "host": "0.0.0.0", "port": 5020, - "ignore_missing_slaves": false, + "ignore_missing_devices": false, "framer": "socket", "identity": { "VendorName": "pymodbus", diff --git a/doc/source/roadmap.rst b/doc/source/roadmap.rst index 1a1cfb1fc..01cc6b470 100644 --- a/doc/source/roadmap.rst +++ b/doc/source/roadmap.rst @@ -20,12 +20,11 @@ The following bullet points are what the maintainers focus on: - 4.0.0, with: - Simulator datastore, with simple configuration - Remove remote_datastore - - Remove BinaryPayload - Server becomes Simulator - client async with sync/async API - Only one datastore, but with different API`s - 4.1.0, with: - - ModbusControlBlock pr slave + - ModbusControlBlock pr device - New custom PDU (function codes) - New serial forwarder - GUI client, to analyze devices diff --git a/doc/source/upgrade_40.rst b/doc/source/upgrade_40.rst new file mode 100644 index 000000000..5036a1299 --- /dev/null +++ b/doc/source/upgrade_40.rst @@ -0,0 +1,47 @@ +Pymodbus 4.0 upgrade procedure +============================== + +Pymodbus 4.0 contains a number of incompatibilities with Pymodbus 3.x, however +most of these are simple edits. + +Python 3.9 +---------- +Python 3.9 is reaching end of life and from october 2025 no longer receives security updates. + +Pymodbus starting with v4.0 start using python 3.10 features, and thus users need to update to +at least python v3.10 + +Users that cannot upgrade the python version, should not upgrade pymodbus to v4.X + + +StartServer +-------------- +custom_funcion= is changed to custom_pdu= and is handled by ModbusServer. + + +payload classes removed +----------------------- +Please replace by result.convert_from_registers() and/or convert_to_registers() + + +Simple replacements +------------------- + +please replace +- slave= with device_id= +- slaves= with device_ids= +- ModbusServerContext(slaves=) with ModbusServerContext(devices=) + +please rename +- ModbusSlaveContext to ModbusDeviceContext +- RemoteSlaveContext to RemoteDeviceContext +- report_slave_id() with report_device_id() +- diag_read_slave_message_count with diag_read_device_message_count +- diag_read_slave_no_response_count with diag_read_device_no_response_count +- diag_read_slave_nak_count with diag_read_device_nak_count +- diag_read_slave_busy_count with diag_read_device_busy_count +- ReturnSlaveMessageCountRequest with ReturnDeviceMessageCountRequest +- ReturnSlaveNoResponseCountRequest with ReturnDeviceNoResponseCountRequest +- ReturnSlaveNAKCountRequest with ReturnDeviceNAKCountRequest +- ReturnSlaveBusyCountRequest with ReturnDeviceBusyCountRequest +- ReturnSlaveBusCharacterOverrunCountRequest with ReturnDeviceBusCharacterOverrunCountRequest diff --git a/examples/client_async.py b/examples/client_async.py index e59a75ca6..3aab14949 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -125,9 +125,9 @@ async def run_async_client(client, modbus_calls=None): async def run_a_few_calls(client): """Test connection works.""" - rr = await client.read_coils(32, count=1, slave=1) + rr = await client.read_coils(32, count=1, device_id=1) assert len(rr.bits) == 8 - rr = await client.read_holding_registers(4, count=2, slave=1) + rr = await client.read_holding_registers(4, count=2, device_id=1) assert rr.registers[0] == 17 assert rr.registers[1] == 17 diff --git a/examples/client_async_calls.py b/examples/client_async_calls.py index 80f6abc5b..b5623ef3d 100755 --- a/examples/client_async_calls.py +++ b/examples/client_async_calls.py @@ -50,7 +50,7 @@ _logger.setLevel("DEBUG") -SLAVE = 0x01 +DEVICE_ID = 0x01 # -------------------------------------------------- @@ -60,7 +60,7 @@ async def async_template_call(client): """Show complete modbus call, async version.""" try: - rr = await client.read_coils(1, count=1, slave=SLAVE) + rr = await client.read_coils(1, count=1, device_id=DEVICE_ID) except ModbusException as exc: txt = f"ERROR: exception in pymodbus {exc}" _logger.error(txt) @@ -81,30 +81,30 @@ async def async_template_call(client): async def async_handle_coils(client): """Read/Write coils.""" _logger.info("### Reading Coil different number of bits (return 8 bits multiples)") - rr = await client.read_coils(1, count=1, slave=SLAVE) + rr = await client.read_coils(1, count=1, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 8 - rr = await client.read_coils(1, count=5, slave=SLAVE) + rr = await client.read_coils(1, count=5, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 8 - rr = await client.read_coils(1, count=12, slave=SLAVE) + rr = await client.read_coils(1, count=12, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 16 - rr = await client.read_coils(1, count=17, slave=SLAVE) + rr = await client.read_coils(1, count=17, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 24 _logger.info("### Write false/true to coils and read to verify") - await client.write_coil(0, True, slave=SLAVE) - rr = await client.read_coils(0, count=1, slave=SLAVE) + await client.write_coil(0, True, device_id=DEVICE_ID) + rr = await client.read_coils(0, count=1, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.bits[0] # test the expected value - await client.write_coils(1, [True] * 21, slave=SLAVE) - rr = await client.read_coils(1, count=21, slave=SLAVE) + await client.write_coils(1, [True] * 21, device_id=DEVICE_ID) + rr = await client.read_coils(1, count=21, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK resp = [True] * 21 # If the returned output quantity is not a multiple of eight, @@ -114,8 +114,8 @@ async def async_handle_coils(client): assert rr.bits == resp # test the expected value _logger.info("### Write False to address 1-8 coils") - await client.write_coils(1, [False] * 8, slave=SLAVE) - rr = await client.read_coils(1, count=8, slave=SLAVE) + await client.write_coils(1, [False] * 8, device_id=DEVICE_ID) + rr = await client.read_coils(1, count=8, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.bits == [False] * 8 # test the expected value @@ -123,7 +123,7 @@ async def async_handle_coils(client): async def async_handle_discrete_input(client): """Read discrete inputs.""" _logger.info("### Reading discrete input, Read address:0-7") - rr = await client.read_discrete_inputs(0, count=8, slave=SLAVE) + rr = await client.read_discrete_inputs(0, count=8, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 8 @@ -131,25 +131,25 @@ async def async_handle_discrete_input(client): async def async_handle_holding_registers(client): """Read/write holding registers.""" _logger.info("### write holding register and read holding registers") - await client.write_register(1, 10, slave=SLAVE) - rr = await client.read_holding_registers(1, count=1, slave=SLAVE) + await client.write_register(1, 10, device_id=DEVICE_ID) + rr = await client.read_holding_registers(1, count=1, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.registers[0] == 10 - await client.write_registers(1, [10] * 8, slave=SLAVE) - rr = await client.read_holding_registers(1, count=8, slave=SLAVE) + await client.write_registers(1, [10] * 8, device_id=DEVICE_ID) + rr = await client.read_holding_registers(1, count=8, device_id=DEVICE_ID) 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, count=1, slave=SLAVE) + await client.write_registers(1, [10], device_id=DEVICE_ID) + rr = await client.read_holding_registers(1, count=1, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.registers == [10] value_int32 = 13211 registers = client.convert_to_registers(value_int32, client.DATATYPE.INT32) - await client.write_registers(1, registers, slave=SLAVE) - rr = await client.read_holding_registers(1, count=len(registers), slave=SLAVE) + await client.write_registers(1, registers, device_id=DEVICE_ID) + rr = await client.read_holding_registers(1, count=len(registers), device_id=DEVICE_ID) assert not rr.isError() # test that call was OK value = client.convert_from_registers(rr.registers, client.DATATYPE.INT32) assert value_int32 == value @@ -161,27 +161,27 @@ async def async_handle_holding_registers(client): "write_address": 1, "values": [256, 128, 100, 50, 25, 10, 5, 1], } - await client.readwrite_registers(slave=SLAVE, **arguments) - rr = await client.read_holding_registers(1, count=8, slave=SLAVE) + await client.readwrite_registers(device_id=DEVICE_ID, **arguments) + rr = await client.read_holding_registers(1, count=8, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.registers == arguments["values"] async def async_write_registers_mypy(client: ModbusBaseClient) -> None: """Read/write holding registers.""" regs1: list[int] = [10] * 8 - await client.write_registers(1, regs1, slave=SLAVE) - rr = await client.read_holding_registers(1, count=len(regs1), slave=SLAVE) + await client.write_registers(1, regs1, device_id=DEVICE_ID) + rr = await client.read_holding_registers(1, count=len(regs1), device_id=DEVICE_ID) assert not rr.isError() # test that call was OK # regs2: list[bytes] = [b'\x01\x02', b'\x03\x04'] - # await client.write_registers(1, regs2, slave=SLAVE) + # await client.write_registers(1, regs2, device_id=DEVICE_ID) # NOT ALLOWED async def async_handle_input_registers(client): """Read input registers.""" _logger.info("### read input registers") - rr = await client.read_input_registers(1, count=8, slave=SLAVE) + rr = await client.read_input_registers(1, count=8, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.registers) == 8 @@ -190,7 +190,7 @@ async def async_handle_file_records(client): """Read/write file records.""" _logger.info("### Read/write file records") record = FileRecord(file_number=14, record_number=12, record_length=64) - rr = await client.read_file_record([record, record], slave=SLAVE) + rr = await client.read_file_record([record, record], device_id=DEVICE_ID) assert not rr.isError() assert len(rr.records) == 2 assert rr.records[0].record_data == b'SERVER DUMMY RECORD.' @@ -198,7 +198,7 @@ async def async_handle_file_records(client): record.record_data = b'Pure test ' record.record_length = len(record.record_data) // 2 record = FileRecord(file_number=14, record_number=12, record_data=b'Pure test ') - rr = await client.write_file_record([record], slave=1) + rr = await client.write_file_record([record], device_id=1) assert not rr.isError() @@ -207,24 +207,24 @@ async def async_handle_file_records(client): async def async_execute_information_requests(client): """Execute extended information requests.""" _logger.info("### Running information requests.") - rr = await client.read_device_information(slave=SLAVE, read_code=1, object_id=0) + rr = await client.read_device_information(device_id=DEVICE_ID, read_code=1, object_id=0) assert not rr.isError() # test that call was OK assert rr.information[0] == b"Pymodbus" - rr = await client.report_slave_id(slave=SLAVE) + rr = await client.report_device_id(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.status - rr = await client.read_exception_status(slave=SLAVE) + rr = await client.read_exception_status(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert not rr.status - rr = await client.diag_get_comm_event_counter(slave=SLAVE) + rr = await client.diag_get_comm_event_counter(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.status assert not rr.count - rr = await client.diag_get_comm_event_log(slave=SLAVE) + rr = await client.diag_get_comm_event_log(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.status assert not (rr.event_count + rr.message_count + len(rr.events)) @@ -234,39 +234,39 @@ async def async_execute_diagnostic_requests(client): """Execute extended diagnostic requests.""" _logger.info("### Running diagnostic requests.") message = b"OK" - rr = await client.diag_query_data(msg=message, slave=SLAVE) + rr = await client.diag_query_data(msg=message, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.message == message - rr = await client.diag_restart_communication(True, slave=SLAVE) + rr = await client.diag_restart_communication(True, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_read_diagnostic_register(slave=SLAVE) + rr = await client.diag_read_diagnostic_register(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_change_ascii_input_delimeter(slave=SLAVE) + rr = await client.diag_change_ascii_input_delimeter(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK rr = await client.diag_clear_counters() assert not rr.isError() # test that call was OK - rr = await client.diag_read_bus_comm_error_count(slave=SLAVE) + rr = await client.diag_read_bus_comm_error_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_read_bus_exception_error_count(slave=SLAVE) + rr = await client.diag_read_bus_exception_error_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_read_slave_message_count(slave=SLAVE) + rr = await client.diag_read_device_message_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_read_slave_no_response_count(slave=SLAVE) + rr = await client.diag_read_device_no_response_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_read_slave_nak_count(slave=SLAVE) + rr = await client.diag_read_device_nak_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_read_slave_busy_count(slave=SLAVE) + rr = await client.diag_read_device_busy_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_read_bus_char_overrun_count(slave=SLAVE) + rr = await client.diag_read_bus_char_overrun_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_read_iop_overrun_count(slave=SLAVE) + rr = await client.diag_read_iop_overrun_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_clear_overrun_counter(slave=SLAVE) + rr = await client.diag_clear_overrun_counter(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_getclear_modbus_response(slave=SLAVE) + rr = await client.diag_getclear_modbus_response(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = await client.diag_force_listen_only(slave=SLAVE, no_response_expected=True) + rr = await client.diag_force_listen_only(device_id=DEVICE_ID, no_response_expected=True) assert rr.isError() # test that call was OK, error indicate no response diff --git a/examples/client_calls.py b/examples/client_calls.py index e720c1398..05f652f6e 100755 --- a/examples/client_calls.py +++ b/examples/client_calls.py @@ -49,7 +49,7 @@ _logger.setLevel("DEBUG") -SLAVE = 0x01 +DEVICE_ID = 0x01 # -------------------------------------------------- @@ -59,7 +59,7 @@ def template_call(client): """Show complete modbus call, sync version.""" try: - rr = client.read_coils(32, count=1, slave=SLAVE) + rr = client.read_coils(32, count=1, device_id=DEVICE_ID) except client_sync.ModbusException as exc: txt = f"ERROR: exception in pymodbus {exc}" _logger.error(txt) @@ -80,30 +80,30 @@ def template_call(client): def handle_coils(client): """Read/Write coils.""" _logger.info("### Reading Coil different number of bits (return 8 bits multiples)") - rr = client.read_coils(1, count=1, slave=SLAVE) + rr = client.read_coils(1, count=1, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 8 - rr = client.read_coils(1, count=5, slave=SLAVE) + rr = client.read_coils(1, count=5, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 8 - rr = client.read_coils(1, count=12, slave=SLAVE) + rr = client.read_coils(1, count=12, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 16 - rr = client.read_coils(1, count=17, slave=SLAVE) + rr = client.read_coils(1, count=17, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 24 _logger.info("### Write false/true to coils and read to verify") - client.write_coil(0, True, slave=SLAVE) - rr = client.read_coils(0, count=1, slave=SLAVE) + client.write_coil(0, True, device_id=DEVICE_ID) + rr = client.read_coils(0, count=1, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.bits[0] # test the expected value - client.write_coils(1, [True] * 21, slave=SLAVE) - rr = client.read_coils(1, count=21, slave=SLAVE) + client.write_coils(1, [True] * 21, device_id=DEVICE_ID) + rr = client.read_coils(1, count=21, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK resp = [True] * 21 # If the returned output quantity is not a multiple of eight, @@ -113,8 +113,8 @@ def handle_coils(client): assert rr.bits == resp # test the expected value _logger.info("### Write False to address 1-8 coils") - client.write_coils(1, [False] * 8, slave=SLAVE) - rr = client.read_coils(1, count=8, slave=SLAVE) + client.write_coils(1, [False] * 8, device_id=DEVICE_ID) + rr = client.read_coils(1, count=8, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.bits == [False] * 8 # test the expected value @@ -122,7 +122,7 @@ def handle_coils(client): def handle_discrete_input(client): """Read discrete inputs.""" _logger.info("### Reading discrete input, Read address:0-7") - rr = client.read_discrete_inputs(0, count=8, slave=SLAVE) + rr = client.read_discrete_inputs(0, count=8, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.bits) == 8 @@ -130,15 +130,15 @@ def handle_discrete_input(client): def handle_holding_registers(client): """Read/write holding registers.""" _logger.info("### write holding register and read holding registers") - client.write_register(1, 10, slave=SLAVE) - rr = client.read_holding_registers(1, count=1, slave=SLAVE) + client.write_register(1, 10, device_id=DEVICE_ID) + rr = client.read_holding_registers(1, count=1, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.registers[0] == 10 value_int32 = 13211 registers = client.convert_to_registers(value_int32, client.DATATYPE.INT32) - client.write_registers(1, registers, slave=SLAVE) - rr = client.read_holding_registers(1, count=len(registers), slave=SLAVE) + client.write_registers(1, registers, device_id=DEVICE_ID) + rr = client.read_holding_registers(1, count=len(registers), device_id=DEVICE_ID) assert not rr.isError() # test that call was OK value = client.convert_from_registers(rr.registers, client.DATATYPE.INT32) assert value_int32 == value @@ -150,8 +150,8 @@ def handle_holding_registers(client): "write_address": 1, "values": [256, 128, 100, 50, 25, 10, 5, 1], } - client.readwrite_registers(slave=SLAVE, **arguments) - rr = client.read_holding_registers(1, count=8, slave=SLAVE) + client.readwrite_registers(device_id=DEVICE_ID, **arguments) + rr = client.read_holding_registers(1, count=8, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.registers == arguments["values"] @@ -159,7 +159,7 @@ def handle_holding_registers(client): def handle_input_registers(client): """Read input registers.""" _logger.info("### read input registers") - rr = client.read_input_registers(1, count=8, slave=SLAVE) + rr = client.read_input_registers(1, count=8, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert len(rr.registers) == 8 @@ -168,7 +168,7 @@ def handle_file_records(client): """Read/write file records.""" _logger.info("### Read/write file records") record = FileRecord(file_number=14, record_number=12, record_length=64) - rr = client.read_file_record([record, record], slave=SLAVE) + rr = client.read_file_record([record, record], device_id=DEVICE_ID) assert not rr.isError() assert len(rr.records) == 2 assert rr.records[0].record_data == b'SERVER DUMMY RECORD.' @@ -176,31 +176,31 @@ def handle_file_records(client): record.record_data = b'Pure test ' record.record_length = len(record.record_data) // 2 record = FileRecord(file_number=14, record_number=12, record_data=b'Pure test ') - rr = client.write_file_record([record], slave=1) + rr = client.write_file_record([record], device_id=1) assert not rr.isError() def execute_information_requests(client): """Execute extended information requests.""" _logger.info("### Running information requests.") - rr = client.read_device_information(slave=SLAVE, read_code=1, object_id=0) + rr = client.read_device_information(device_id=DEVICE_ID, read_code=1, object_id=0) assert not rr.isError() # test that call was OK assert rr.information[0] == b"Pymodbus" - rr = client.report_slave_id(slave=SLAVE) + rr = client.report_device_id(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.status - rr = client.read_exception_status(slave=SLAVE) + rr = client.read_exception_status(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert not rr.status - rr = client.diag_get_comm_event_counter(slave=SLAVE) + rr = client.diag_get_comm_event_counter(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.status assert not rr.count - rr = client.diag_get_comm_event_log(slave=SLAVE) + rr = client.diag_get_comm_event_log(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK assert rr.status assert not (rr.event_count + rr.message_count + len(rr.events)) @@ -211,39 +211,39 @@ def execute_diagnostic_requests(client): _logger.info("### Running diagnostic requests.") # NOT WORKING: ONLY SYNC # message = b"OK" - # rr = client.diag_query_data(msg=message, slave=SLAVE) + # rr = client.diag_query_data(msg=message, device_id=DEVICE_ID) # assert not rr.isError() # test that call was OK # assert rr.message == message - rr = client.diag_restart_communication(True, slave=SLAVE) + rr = client.diag_restart_communication(True, device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_read_diagnostic_register(slave=SLAVE) + rr = client.diag_read_diagnostic_register(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_change_ascii_input_delimeter(slave=SLAVE) + rr = client.diag_change_ascii_input_delimeter(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK rr = client.diag_clear_counters() assert not rr.isError() # test that call was OK - rr = client.diag_read_bus_comm_error_count(slave=SLAVE) + rr = client.diag_read_bus_comm_error_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_read_bus_exception_error_count(slave=SLAVE) + rr = client.diag_read_bus_exception_error_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_read_slave_message_count(slave=SLAVE) + rr = client.diag_read_device_message_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_read_slave_no_response_count(slave=SLAVE) + rr = client.diag_read_device_no_response_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_read_slave_nak_count(slave=SLAVE) + rr = client.diag_read_device_nak_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_read_slave_busy_count(slave=SLAVE) + rr = client.diag_read_device_busy_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_read_bus_char_overrun_count(slave=SLAVE) + rr = client.diag_read_bus_char_overrun_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_read_iop_overrun_count(slave=SLAVE) + rr = client.diag_read_iop_overrun_count(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - rr = client.diag_clear_overrun_counter(slave=SLAVE) + rr = client.diag_clear_overrun_counter(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - # NOT WORKING rr = client.diag_getclear_modbus_response(slave=SLAVE) + # NOT WORKING rr = client.diag_getclear_modbus_response(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK - # NOT WORKING: rr = client.diag_force_listen_only(slave=SLAVE) + # NOT WORKING: rr = client.diag_force_listen_only(device_id=DEVICE_ID) assert not rr.isError() # test that call was OK diff --git a/examples/client_performance.py b/examples/client_performance.py index febaf5869..78a78e65e 100755 --- a/examples/client_performance.py +++ b/examples/client_performance.py @@ -37,7 +37,7 @@ def run_sync_client_test(): start_time = time.time() for _i in range(LOOP_COUNT): - rr = client.read_input_registers(1, count=REGISTER_COUNT, slave=1) + rr = client.read_input_registers(1, count=REGISTER_COUNT, device_id=1) if rr.isError(): print(f"Received Modbus library error({rr})") break @@ -64,7 +64,7 @@ async def run_async_client_test(): start_time = time.time() for _i in range(LOOP_COUNT): - rr = await client.read_input_registers(1, count=REGISTER_COUNT, slave=1) + rr = await client.read_input_registers(1, count=REGISTER_COUNT, device_id=1) if rr.isError(): print(f"Received Modbus library error({rr})") break diff --git a/examples/client_sync.py b/examples/client_sync.py index c64234f0a..ce4ded406 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -128,9 +128,9 @@ def run_sync_client(client, modbus_calls=None): def run_a_few_calls(client): """Test connection works.""" try: - rr = client.read_coils(32, count=1, slave=1) + rr = client.read_coils(32, count=1, device_id=1) assert len(rr.bits) == 8 - rr = client.read_holding_registers(4, count=2, slave=1) + rr = client.read_holding_registers(4, count=2, device_id=1) assert rr.registers[0] == 17 assert rr.registers[1] == 17 except ModbusException as exc: diff --git a/examples/contrib/drainage_sim.py b/examples/contrib/drainage_sim.py index 8e09dd2a4..e69a57f08 100755 --- a/examples/contrib/drainage_sim.py +++ b/examples/contrib/drainage_sim.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Simulates two Modbus TCP slave servers: +# Simulates two Modbus TCP device servers: # # Port 5020: Digital IO (DIO) with 8 discrete inputs and 8 coils. The first two coils each control # a simulated pump. Inputs are not used. @@ -12,7 +12,7 @@ import logging from datetime import datetime -from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext +from pymodbus.datastore import ModbusSequentialDataBlock, ModbusDeviceContext, ModbusServerContext from pymodbus.server import StartAsyncTcpServer INITIAL_WATER_LEVEL = 300 @@ -23,9 +23,9 @@ dio_di = ModbusSequentialDataBlock(1, [False] * 8) dio_co = ModbusSequentialDataBlock(1, [False] * 8) -dio_context = ModbusSlaveContext(di = dio_di, co = dio_co) +dio_context = ModbusDeviceContext(di = dio_di, co = dio_co) wlm_ir = ModbusSequentialDataBlock(1, [INITIAL_WATER_LEVEL]) -wlm_context = ModbusSlaveContext(ir = wlm_ir) +wlm_context = ModbusDeviceContext(ir = wlm_ir) async def update(): while True: @@ -50,13 +50,13 @@ async def log(): logging.info(f"{datetime.now()}: WLM water level: {wlm_level}, DIO outputs: {dio_outputs}") async def run(): - ctx = ModbusServerContext(slaves = dio_context) + ctx = ModbusServerContext(device_ids = dio_context) dio_server = asyncio.create_task(StartAsyncTcpServer(context = ctx, address = ("0.0.0.0", 5020))) - logging.info("Initialising slave server DIO on port 5020") + logging.info("Initialising device server DIO on port 5020") - ctx = ModbusServerContext(slaves = wlm_context) + ctx = ModbusServerContext(device_ids = wlm_context) wlm_server = asyncio.create_task(StartAsyncTcpServer(context = ctx, address = ("0.0.0.0", 5021))) - logging.info("Initialising slave server WLM on port 5021") + logging.info("Initialising device server WLM on port 5021") update_task = asyncio.create_task(update()) logging_task = asyncio.create_task(log()) diff --git a/examples/contrib/serial_forwarder.py b/examples/contrib/serial_forwarder.py index 325570634..9b3db60bc 100644 --- a/examples/contrib/serial_forwarder.py +++ b/examples/contrib/serial_forwarder.py @@ -1,7 +1,7 @@ """Pymodbus SerialRTU2TCP Forwarder usage : -python3 serial_forwarder.py --log DEBUG --port "/dev/ttyUSB0" --baudrate 9600 --server_ip "192.168.1.27" --server_port 5020 --slaves 1 2 3 +python3 serial_forwarder.py --log DEBUG --port "/dev/ttyUSB0" --baudrate 9600 --server_ip "192.168.1.27" --server_port 5020 --device_ids 1 2 3 """ import argparse import asyncio @@ -10,7 +10,7 @@ from pymodbus.client import ModbusSerialClient from pymodbus.datastore import ModbusServerContext -from pymodbus.datastore.remote import RemoteSlaveContext +from pymodbus.datastore.remote import RemoteDeviceContext from pymodbus.server import ModbusTcpServer @@ -32,21 +32,21 @@ def __init__(self): async def run(self): """Run the server""" - port, baudrate, server_port, server_ip, slaves = get_commandline() + port, baudrate, server_port, server_ip, device_ids = get_commandline() client = ModbusSerialClient(method="rtu", port=port, baudrate=baudrate) message = f"RTU bus on {port} - baudrate {baudrate}" _logger.info(message) store = {} - for i in slaves: - store[i] = RemoteSlaveContext(client, slave=i) - context = ModbusServerContext(slaves=store, single=False) + for i in device_ids: + store[i] = RemoteDeviceContext(client, device_id=i) + context = ModbusServerContext(device_ids=store, single=False) self.server = ModbusTcpServer( context, address=(server_ip, server_port), ) message = f"serving on {server_ip} port {server_port}" _logger.info(message) - message = f"listening to slaves {context.slaves()}" + message = f"listening to device_ids {context.device_ids()}" _logger.info(message) await self.server.serve_forever() @@ -74,16 +74,16 @@ def get_commandline(): parser.add_argument("--server_port", help="server port", default=5020, type=int) parser.add_argument("--server_ip", help="server IP", default="127.0.0.1", type=str) parser.add_argument( - "--slaves", help="list of slaves to forward", type=int, nargs="+" + "--sdevice_ids", help="list of device_ids to forward", type=int, nargs="+" ) args = parser.parse_args() # set defaults _logger.setLevel(args.log.upper()) - if not args.slaves: - args.slaves = {1, 2, 3} - return args.port, args.baudrate, args.server_port, args.server_ip, args.slaves + if not args.device_ids: + args.device_ids = {1, 2, 3} + return args.port, args.baudrate, args.server_port, args.server_ip, args.device_ids if __name__ == "__main__": diff --git a/examples/contrib/solar.py b/examples/contrib/solar.py index 3106317eb..a3aab32f6 100755 --- a/examples/contrib/solar.py +++ b/examples/contrib/solar.py @@ -59,7 +59,7 @@ def main() -> None: def solar_calls(client: ModbusTcpClient) -> None: """Read registers.""" error = False - + for addr, format, factor, comment, unit in ( # data_type according to ModbusClientMixin.DATATYPE.value[0] (32008, "H", 1, "Alarm 1", "(bitfield)"), (32009, "H", 1, "Alarm 2", "(bitfield)"), @@ -79,15 +79,15 @@ def solar_calls(client: ModbusTcpClient) -> None: sleep(0.1) client.connect() sleep(1) - + data_type = get_data_type(format) count = data_type.value[1] var_type = data_type.name _logger.info(f"*** Reading {comment} ({var_type})") - + try: - rr = client.read_holding_registers(address=addr, count=count, slave=1) + rr = client.read_holding_registers(address=addr, count=count, device_id=1) except ModbusException as exc: _logger.error(f"Modbus exception: {exc!s}") error = True @@ -100,7 +100,7 @@ def solar_calls(client: ModbusTcpClient) -> None: _logger.error(f"Response exception: {rr!s}") error = True continue - + value = client.convert_from_registers(rr.registers, data_type) * factor if factor < 1: value = round(value, int(log10(factor) * -1)) diff --git a/examples/custom_msg.py b/examples/custom_msg.py index 76af0d16f..48624d0a5 100755 --- a/examples/custom_msg.py +++ b/examples/custom_msg.py @@ -16,9 +16,9 @@ from pymodbus import FramerType from pymodbus.client import AsyncModbusTcpClient as ModbusClient from pymodbus.datastore import ( + ModbusDeviceContext, ModbusSequentialDataBlock, ModbusServerContext, - ModbusSlaveContext, ) from pymodbus.exceptions import ModbusIOException from pymodbus.pdu import ModbusPDU @@ -43,9 +43,9 @@ class CustomModbusResponse(ModbusPDU): function_code = 55 rtu_byte_count_pos = 2 - def __init__(self, values=None, slave=1, transaction=0): + def __init__(self, values=None, device_id=1, transaction=0): """Initialize.""" - super().__init__(dev_id=slave, transaction_id=transaction) + super().__init__(dev_id=device_id, transaction_id=transaction) self.values = values or [] def encode(self): @@ -75,9 +75,9 @@ class CustomRequest(ModbusPDU): function_code = 55 rtu_frame_size = 8 - def __init__(self, address=None, slave=1, transaction=0): + def __init__(self, address=None, device_id=1, transaction=0): """Initialize.""" - super().__init__(dev_id=slave, transaction_id=transaction) + super().__init__(dev_id=device_id, transaction_id=transaction) self.address = address self.count = 2 @@ -89,7 +89,7 @@ def decode(self, data): """Decode.""" self.address, self.count = struct.unpack(">HH", data) - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Execute.""" _ = context return CustomModbusResponse() @@ -103,12 +103,12 @@ async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: class Read16CoilsRequest(ReadCoilsRequest): """Read 16 coils in one request.""" - def __init__(self, address, slave=1, transaction=0): + def __init__(self, address, device_id=1, transaction=0): """Initialize a new instance. :param address: The address to start reading from """ - super().__init__(address=address, count=16, dev_id=slave, transaction_id=transaction) + super().__init__(address=address, count=16, dev_id=device_id, transaction_id=transaction) # --------------------------------------------------------------------------- # @@ -121,7 +121,7 @@ def __init__(self, address, slave=1, transaction=0): async def main(host="localhost", port=5020): """Run versions of read coil.""" - store = ModbusServerContext(slaves=ModbusSlaveContext( + store = ModbusServerContext(devices=ModbusDeviceContext( di=ModbusSequentialDataBlock(0, [17] * 100), co=ModbusSequentialDataBlock(0, [17] * 100), hr=ModbusSequentialDataBlock(0, [17] * 100), @@ -140,8 +140,8 @@ async def main(host="localhost", port=5020): # add new modbus function code. client.register(CustomModbusResponse) - slave=1 - request1 = CustomRequest(32, slave=slave) + device_id=1 + request1 = CustomRequest(32, device_id=device_id) try: result = await client.execute(False, request1) except ModbusIOException: @@ -150,7 +150,7 @@ async def main(host="localhost", port=5020): print(result) # inherited request - request2 = Read16CoilsRequest(32, slave) + request2 = Read16CoilsRequest(32, device_id) result = await client.execute(False, request2) print(result) await ServerAsyncStop() diff --git a/examples/datastore_simulator_share.py b/examples/datastore_simulator_share.py index a74f7af8b..247c4a690 100755 --- a/examples/datastore_simulator_share.py +++ b/examples/datastore_simulator_share.py @@ -32,9 +32,8 @@ import asyncio import logging -from pymodbus import pymodbus_apply_logging_config +from pymodbus import ModbusDeviceIdentification, pymodbus_apply_logging_config from pymodbus.datastore import ModbusServerContext, ModbusSimulatorContext -from pymodbus.device import ModbusDeviceIdentification from pymodbus.server import StartAsyncTcpServer @@ -153,7 +152,7 @@ def setup_simulator(setup=None, actions=None, cmdline=None): args.port = int(args.port) context = ModbusSimulatorContext(setup, actions) - args.context = ModbusServerContext(slaves=context, single=True) + args.context = ModbusServerContext(devices=context, single=True) args.identity = ModbusDeviceIdentification( info_name={ "VendorName": "Pymodbus", diff --git a/examples/helper.py b/examples/helper.py index 2f6fabc7a..93fe05649 100755 --- a/examples/helper.py +++ b/examples/helper.py @@ -75,8 +75,8 @@ def get_commandline(server: bool = False, description: str | None = None, extras type=str, ) parser.add_argument( - "--slaves", - help="set number of slaves, default is 0 (any)", + "--device_ids", + help="set number of device_ids, default is 0 (any)", default=0, type=int, ) diff --git a/examples/modbus_forwarder.py b/examples/modbus_forwarder.py index 2a98e16da..b72170a17 100755 --- a/examples/modbus_forwarder.py +++ b/examples/modbus_forwarder.py @@ -33,7 +33,7 @@ from pymodbus.client import ModbusTcpClient from pymodbus.datastore import ModbusServerContext -from pymodbus.datastore.remote import RemoteSlaveContext +from pymodbus.datastore.remote import RemoteDeviceContext from pymodbus.server import StartAsyncTcpServer @@ -56,18 +56,18 @@ async def run_forwarder(args): ) args.client.connect() assert args.client.connected - # If required to communicate with a specified client use slave= - # in RemoteSlaveContext - # For e.g to forward the requests to slave with slave address 1 use - # store = RemoteSlaveContext(client, slave=1) - store: dict | RemoteSlaveContext - if args.slaves: + # If required to communicate with a specified client use device_id= + # in RemoteDeviceContext + # For e.g to forward the requests to device_id with device address 1 use + # store = RemoteDeviceContext(client, device_id=1) + store: dict | RemoteDeviceContext + if args.device_ids: store = {} - for i in args.slaves: - store[i.to_bytes(1, "big")] = RemoteSlaveContext(args.client, slave=i) + for i in args.device_ids: + store[i.to_bytes(1, "big")] = RemoteDeviceContext(args.client, device_id=i) else: - store = RemoteSlaveContext(args.client, slave=1) - args.context = ModbusServerContext(slaves=store, single=True) + store = RemoteDeviceContext(args.client, device_id=1) + args.context = ModbusServerContext(devices=store, single=True) await StartAsyncTcpServer(context=args.context, address=("", args.port)) # loop forever diff --git a/examples/package_test_tool.py b/examples/package_test_tool.py index 8296c432d..5c06f4104 100755 --- a/examples/package_test_tool.py +++ b/examples/package_test_tool.py @@ -49,13 +49,17 @@ import pymodbus.client as modbusClient import pymodbus.server as modbusServer -from pymodbus import FramerType, ModbusException, pymodbus_apply_logging_config +from pymodbus import ( + FramerType, + ModbusDeviceIdentification, + ModbusException, + pymodbus_apply_logging_config, +) from pymodbus.datastore import ( + ModbusDeviceContext, ModbusSequentialDataBlock, ModbusServerContext, - ModbusSlaveContext, ) -from pymodbus.device import ModbusDeviceIdentification from pymodbus.logging import Log from pymodbus.transport import NULLMODEM_HOST, CommParams, CommType, ModbusProtocol @@ -147,13 +151,13 @@ def __init__(self, comm: CommType): """Initialize runtime tester.""" global test_port # pylint: disable=global-statement self.comm = comm - self.store = ModbusSlaveContext( + self.store = ModbusDeviceContext( di=ModbusSequentialDataBlock(0, [17] * 100), co=ModbusSequentialDataBlock(0, [17] * 100), hr=ModbusSequentialDataBlock(0, [17] * 100), ir=ModbusSequentialDataBlock(0, [17] * 100), ) - self.context = ModbusServerContext(slaves=self.store, single=True) + self.context = ModbusServerContext(devices=self.store, single=True) self.identity = ModbusDeviceIdentification( info_name={"VendorName": "VendorName"} ) @@ -210,7 +214,7 @@ async def client_calls(client): """Test client API.""" Log.debug("--> Client calls starting.") try: - resp = await client.read_holding_registers(address=124, count=4, slave=1) + resp = await client.read_holding_registers(address=124, count=4, device_id=1) except ModbusException as exc: txt = f"ERROR: exception in pymodbus {exc}" Log.error(txt) diff --git a/examples/server_async.py b/examples/server_async.py index bec0135da..d60be6437 100755 --- a/examples/server_async.py +++ b/examples/server_async.py @@ -9,7 +9,7 @@ [--framer {ascii,rtu,socket,tls}] [--log {critical,error,warning,info,debug}] [--port PORT] [--store {sequential,sparse,factory,none}] - [--slaves SLAVES] + [--device_ids DEVICE_IDS] -h, --help show this help message and exit @@ -24,8 +24,8 @@ set serial device baud rate --store {sequential,sparse,factory,none} set datastore type - --slaves SLAVES - set number of slaves to respond to + --device_ids DEVICE IDs + set list of devices to respond to The corresponding client can be started as: @@ -47,14 +47,14 @@ for more information.") sys.exit(-1) +from pymodbus import ModbusDeviceIdentification from pymodbus import __version__ as pymodbus_version from pymodbus.datastore import ( + ModbusDeviceContext, ModbusSequentialDataBlock, ModbusServerContext, - ModbusSlaveContext, ModbusSparseDataBlock, ) -from pymodbus.device import ModbusDeviceIdentification from pymodbus.server import ( StartAsyncSerialServer, StartAsyncTcpServer, @@ -91,17 +91,17 @@ 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 > 1: + if args.device_ids > 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 + # to respond with different device contexts for different device ids. + # By default it will return the same context for every device id supplied # (broadcast mode). # However, this can be overloaded by setting the single flag to False and - # then supplying a dictionary of slave id to context mapping:: + # then supplying a dictionary of device id to context mapping:: context = {} - for slave in range(args.slaves): - context[slave] = ModbusSlaveContext( + for device_id in range(args.device_ids): + context[device_id] = ModbusDeviceContext( di=datablock(), co=datablock(), hr=datablock(), @@ -110,13 +110,13 @@ def setup_server(description=None, context=None, cmdline=None): single = False else: - context = ModbusSlaveContext( + context = ModbusDeviceContext( di=datablock(), co=datablock(), hr=datablock(), ir=datablock() ) single = True # Build data storage - args.context = ModbusServerContext(slaves=context, single=single) + args.context = ModbusServerContext(devices=context, single=single) # ----------------------------------------------------------------------- # # initialize the server information @@ -148,8 +148,8 @@ async def run_async_server(args) -> None: address=address, # listen address # custom_functions=[], # allow custom handling framer=args.framer, # The framer strategy to use - # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat slave 0 as broadcast address, + # ignore_missing_devices=True, # ignore request to a missing device + # broadcast_enable=False, # treat device 0 as broadcast address, # timeout=1, # waiting time for request to complete ) elif args.comm == "udp": @@ -163,8 +163,8 @@ async def run_async_server(args) -> None: address=address, # listen address # custom_functions=[], # allow custom handling framer=args.framer, # The framer strategy to use - # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat slave 0 as broadcast address, + # ignore_missing_devices=True, # ignore request to a missing device + # broadcast_enable=False, # treat device id 0 as broadcast address, # timeout=1, # waiting time for request to complete ) elif args.comm == "serial": @@ -182,8 +182,8 @@ async def run_async_server(args) -> None: # parity="N", # Which kind of parity to use baudrate=args.baudrate, # The baud rate to use for the serial device # handle_local_echo=False, # Handle local echo of the USB-to-RS485 adaptor - # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat slave 0 as broadcast address, + # ignore_missing_devices=True, # ignore request to a missing device + # broadcast_enable=False, # treat device_id 0 as broadcast address, ) elif args.comm == "tls": address = (args.host if args.host else "", args.port if args.port else None) @@ -202,8 +202,8 @@ async def run_async_server(args) -> None: "key" ), # The key file path for TLS (used if sslctx is None) # password="none", # The password for for decrypting the private key file - # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat slave 0 as broadcast address, + # ignore_missing_devices=True, # ignore request to a missing device + # broadcast_enable=False, # treat device_id 0 as broadcast address, # timeout=1, # waiting time for request to complete ) diff --git a/examples/server_callback.py b/examples/server_callback.py index 5b137a6ae..4db283871 100755 --- a/examples/server_callback.py +++ b/examples/server_callback.py @@ -18,9 +18,9 @@ sys.exit(-1) from pymodbus.datastore import ( + ModbusDeviceContext, ModbusSequentialDataBlock, ModbusServerContext, - ModbusSlaveContext, ) @@ -58,8 +58,8 @@ async def run_callback_server(cmdline=None): queue: asyncio.Queue = asyncio.Queue() block = CallbackDataBlock(queue, 0x00, [17] * 100) block.setValues(1, 15) - store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) - context = ModbusServerContext(slaves=store, single=True) + store = ModbusDeviceContext(di=block, co=block, hr=block, ir=block) + context = ModbusServerContext(devices=store, single=True) run_args = server_async.setup_server( description="Run callback server.", cmdline=cmdline, context=context ) diff --git a/examples/server_hook.py b/examples/server_hook.py index 45df6abe4..8c4662ccf 100755 --- a/examples/server_hook.py +++ b/examples/server_hook.py @@ -11,9 +11,9 @@ from pymodbus import FramerType, pymodbus_apply_logging_config from pymodbus.datastore import ( + ModbusDeviceContext, ModbusSequentialDataBlock, ModbusServerContext, - ModbusSlaveContext, ) from pymodbus.pdu import ModbusPDU from pymodbus.server import ModbusTcpServer @@ -48,7 +48,7 @@ async def setup(self): pymodbus_apply_logging_config(logging.DEBUG) datablock = ModbusSequentialDataBlock(0x00, [17] * 100) context = ModbusServerContext( - slaves=ModbusSlaveContext( + devices=ModbusDeviceContext( di=datablock, co=datablock, hr=datablock, ir=datablock ), single=True, diff --git a/examples/server_sync.py b/examples/server_sync.py index c85a92483..6eb47c6ec 100755 --- a/examples/server_sync.py +++ b/examples/server_sync.py @@ -9,7 +9,7 @@ [--framer {ascii,rtu,socket,tls}] [--log {critical,error,warning,info,debug}] [--port PORT] [--store {sequential,sparse,factory,none}] - [--slaves SLAVES] + [--device_ids DEVICE_IDS] -h, --help show this help message and exit @@ -24,8 +24,8 @@ set serial device baud rate --store {sequential,sparse,factory,none} set datastore type - --slaves SLAVES - set number of slaves to respond to + --device_ids DEVICE_IDS + set list of devices to respond to The corresponding client can be started as: python3 client_sync.py @@ -74,8 +74,8 @@ def run_sync_server(args) -> None: address=address, # listen address # custom_functions=[], # allow custom handling framer=args.framer, # The framer strategy to use - # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat slave 0 as broadcast address, + # ignore_missing_devices=True, # ignore request to a missing device + # broadcast_enable=False, # treat device_id 0 as broadcast address, # timeout=1, # waiting time for request to complete ) elif args.comm == "udp": @@ -86,8 +86,8 @@ def run_sync_server(args) -> None: address=address, # listen address # custom_functions=[], # allow custom handling framer=args.framer, # The framer strategy to use - # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat slave 0 as broadcast address, + # ignore_missing_devices=True, # ignore request to a missing device + # broadcast_enable=False, # treat device_id 0 as broadcast address, # timeout=1, # waiting time for request to complete ) elif args.comm == "serial": @@ -105,8 +105,8 @@ def run_sync_server(args) -> None: # parity="E", # Which kind of parity to use baudrate=args.baudrate, # The baud rate to use for the serial device # handle_local_echo=False, # Handle local echo of the USB-to-RS485 adaptor - # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat slave 0 as broadcast address, + # ignore_missing_devices=True, # ignore request to a missing device + # broadcast_enable=False, # treat device_id 0 as broadcast address, ) elif args.comm == "tls": address = ("", args.port) if args.port else None @@ -125,8 +125,8 @@ def run_sync_server(args) -> None: "key" ), # The key file path for TLS (used if sslctx is None) # password=None, # The password for for decrypting the private key file - # ignore_missing_slaves=True, # ignore request to a missing slave - # broadcast_enable=False, # treat slave 0 as broadcast address, + # ignore_missing_devices=True, # ignore request to a missing device + # broadcast_enable=False, # treat device_id 0 as broadcast address, # timeout=1, # waiting time for request to complete ) diff --git a/examples/server_updating.py b/examples/server_updating.py index 4dee58cf1..270b5dcaf 100755 --- a/examples/server_updating.py +++ b/examples/server_updating.py @@ -10,7 +10,7 @@ [--framer {ascii,rtu,socket,tls}] [--log {critical,error,warning,info,debug}] [--port PORT] [--store {sequential,sparse,factory,none}] - [--slaves SLAVES] + [--device_ids DEVICE_IDS] -h, --help show this help message and exit @@ -25,8 +25,8 @@ set serial device baud rate --store {sequential,sparse,factory,none} set datastore type - --slaves SLAVES - set number of slaves to respond to + --device_ids DEVICE_IDS + set number of devices to respond to The corresponding client can be started as: python3 client_sync.py @@ -45,9 +45,9 @@ sys.exit(-1) from pymodbus.datastore import ( + ModbusDeviceContext, ModbusSequentialDataBlock, ModbusServerContext, - ModbusSlaveContext, ) @@ -64,14 +64,14 @@ async def updating_task(context): against concurrent use. """ fc_as_hex = 3 - slave_id = 0x00 + device_id = 0x00 address = 0x10 count = 6 # set values to zero - values = context[slave_id].getValues(fc_as_hex, address, count=count) + values = context[device_id].getValues(fc_as_hex, address, count=count) values = [0 for v in values] - context[slave_id].setValues(fc_as_hex, address, values) + context[device_id].setValues(fc_as_hex, address, values) txt = ( f"updating_task: started: initialised values: {values!s} at address {address!s}" @@ -83,9 +83,9 @@ async def updating_task(context): while True: await asyncio.sleep(2) - values = context[slave_id].getValues(fc_as_hex, address, count=count) + values = context[device_id].getValues(fc_as_hex, address, count=count) values = [v + 1 for v in values] - context[slave_id].setValues(fc_as_hex, address, values) + context[device_id].setValues(fc_as_hex, address, values) txt = f"updating_task: incremented values: {values!s} at address {address!s}" print(txt) @@ -101,8 +101,8 @@ def setup_updating_server(cmdline=None): # Continuing, use a sequential block without gaps. datablock = ModbusSequentialDataBlock(0x00, [17] * 100) - slavecontext = ModbusSlaveContext(di=datablock, co=datablock, hr=datablock, ir=datablock) - context = ModbusServerContext(slaves=slavecontext, single=True) + device_context = ModbusDeviceContext(di=datablock, co=datablock, hr=datablock, ir=datablock) + context = ModbusServerContext(devices=device_context, single=True) return server_async.setup_server( description="Run asynchronous server.", context=context, cmdline=cmdline ) diff --git a/examples/simple_async_client.py b/examples/simple_async_client.py index fceac333f..8cbaa6af8 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -68,7 +68,7 @@ async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): print("get and verify data") try: # See all calls in client_calls.py - rr = await client.read_coils(1, count=1, slave=1) + rr = await client.read_coils(1, count=1, device_id=1) except ModbusException as exc: print(f"Received ModbusException({exc}) from library") client.close() @@ -80,7 +80,7 @@ async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): return try: # See all calls in client_calls.py - rr = await client.read_holding_registers(10, count=2, slave=1) + rr = await client.read_holding_registers(10, count=2, device_id=1) except ModbusException as exc: print(f"Received ModbusException({exc}) from library") client.close() diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index b65071a68..6ca1e365d 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -67,7 +67,7 @@ def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): print("get and verify data") try: - rr = client.read_coils(1, count=1, slave=1) + rr = client.read_coils(1, count=1, device_id=1) except ModbusException as exc: print(f"Received ModbusException({exc}) from library") client.close() @@ -79,7 +79,7 @@ def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): return try: # See all calls in client_calls.py - rr = client.read_holding_registers(10, count=2, slave=1) + rr = client.read_holding_registers(10, count=2, device_id=1) except ModbusException as exc: print(f"Received ModbusException({exc}) from library") client.close() diff --git a/examples/simulator.py b/examples/simulator.py index 4af1fb9cc..a947c17cb 100755 --- a/examples/simulator.py +++ b/examples/simulator.py @@ -23,7 +23,7 @@ async def read_registers( client, addr, count, is_int, curval=None, minval=None, maxval=None ): """Run modbus call.""" - rr = await client.read_holding_registers(addr, count=count, slave=1) + rr = await client.read_holding_registers(addr, count=count, device_id=1) assert not rr.isError() if count == 1: value = rr.registers[0] diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 77f613128..62245eee8 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -6,16 +6,18 @@ __all__ = [ "ExceptionResponse", "FramerType", + "ModbusDeviceIdentification", "ModbusException", "__version__", "__version_full__", - "pymodbus_apply_logging_config", + "pymodbus_apply_logging_config" ] from pymodbus.exceptions import ModbusException from pymodbus.framer import FramerType from pymodbus.logging import pymodbus_apply_logging_config from pymodbus.pdu import ExceptionResponse +from pymodbus.pdu.device import ModbusDeviceIdentification __version__ = "4.0.0dev0" diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 3cdd45cb1..099a9d956 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -145,7 +145,6 @@ def __init__( ModbusClientMixin.__init__(self) # type: ignore[arg-type] self.comm_params = comm_params self.retries = retries - self.slaves: list[int] = [] # Common variables. self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(DecodePDU(False)) diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index 67f717ae0..dcfffbdc4 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -54,32 +54,32 @@ def __init__(self): def execute(self, no_response_expected: bool, request: ModbusPDU) -> T: """Execute request.""" - def read_coils(self, address: int, *, count: int = 1, slave: int = 1, no_response_expected: bool = False) -> T: + def read_coils(self, address: int, *, count: int = 1, device_id: int = 1, no_response_expected: bool = False) -> T: """Read coils (code 0x01). :param address: Start address to read from :param count: (optional) Number of coils to read - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: - reads from 1 to 2000 contiguous in a remote device (slave). + reads from 1 to 2000 contiguous in a remote device. Coils are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0). """ - return self.execute(no_response_expected, pdu_bit.ReadCoilsRequest(address=address, count=count, dev_id=slave)) + return self.execute(no_response_expected, pdu_bit.ReadCoilsRequest(address=address, count=count, dev_id=device_id)) def read_discrete_inputs(self, address: int, *, count: int = 1, - slave: int = 1, + device_id: int = 1, no_response_expected: bool = False) -> T: """Read discrete inputs (code 0x02). :param address: Start address to read from :param count: (optional) Number of coils to read - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -87,20 +87,20 @@ def read_discrete_inputs(self, Discrete Inputs are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0). """ - pdu = pdu_bit.ReadDiscreteInputsRequest(address=address, count=count, dev_id=slave) + pdu = pdu_bit.ReadDiscreteInputsRequest(address=address, count=count, dev_id=device_id) return self.execute(no_response_expected, pdu) def read_holding_registers(self, address: int, *, count: int = 1, - slave: int = 1, + device_id: int = 1, no_response_expected: bool = False) -> T: """Read holding registers (code 0x03). :param address: Start address to read from :param count: (optional) Number of registers to read - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -111,19 +111,19 @@ def read_holding_registers(self, Registers are addressed starting at zero. Therefore devices that specify 1-16 are addressed as 0-15. """ - return self.execute(no_response_expected, pdu_reg.ReadHoldingRegistersRequest(address=address, count=count, dev_id=slave)) + return self.execute(no_response_expected, pdu_reg.ReadHoldingRegistersRequest(address=address, count=count, dev_id=device_id)) def read_input_registers(self, address: int, *, count: int = 1, - slave: int = 1, + device_id: int = 1, no_response_expected: bool = False) -> T: """Read input registers (code 0x04). :param address: Start address to read from :param count: (optional) Number of coils to read - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -134,14 +134,14 @@ def read_input_registers(self, Registers are addressed starting at zero. Therefore devices that specify 1-16 are addressed as 0-15. """ - return self.execute(no_response_expected, pdu_reg.ReadInputRegistersRequest(address=address, count=count, dev_id=slave)) + return self.execute(no_response_expected, pdu_reg.ReadInputRegistersRequest(address=address, count=count, dev_id=device_id)) - def write_coil(self, address: int, value: bool, *, slave: int = 1, no_response_expected: bool = False) -> T: + def write_coil(self, address: int, value: bool, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Write single coil (code 0x05). :param address: Address to write to :param value: Boolean to write - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -149,15 +149,15 @@ def write_coil(self, address: int, value: bool, *, slave: int = 1, no_response_e Coils are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0). """ - pdu = pdu_bit.WriteSingleCoilRequest(address=address, bits=[value], dev_id=slave) + pdu = pdu_bit.WriteSingleCoilRequest(address=address, bits=[value], dev_id=device_id) return self.execute(no_response_expected, pdu) - def write_register(self, address: int, value: int, *, slave: int = 1, no_response_expected: bool = False) -> T: + def write_register(self, address: int, value: int, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Write register (code 0x06). :param address: Address to write to :param value: Value to write - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -168,12 +168,12 @@ def write_register(self, address: int, value: int, *, slave: int = 1, no_respons Registers are addressed starting at zero. Therefore register numbered 1 is addressed as 0. """ - return self.execute(no_response_expected, pdu_reg.WriteSingleRegisterRequest(address=address, registers=[value], dev_id=slave)) + return self.execute(no_response_expected, pdu_reg.WriteSingleRegisterRequest(address=address, registers=[value], dev_id=device_id)) - def read_exception_status(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def read_exception_status(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Read Exception Status (code 0x07). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -183,13 +183,13 @@ def read_exception_status(self, *, slave: int = 1, no_response_expected: bool = accessing this information, because the Exception Output references are known (no output reference is needed in the function). """ - return self.execute(no_response_expected, pdu_other_msg.ReadExceptionStatusRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_other_msg.ReadExceptionStatusRequest(dev_id=device_id)) - def diag_query_data(self, msg: bytes, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_query_data(self, msg: bytes, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose query data (code 0x08 sub 0x00). :param msg: Message to be returned - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -197,13 +197,13 @@ def diag_query_data(self, msg: bytes, *, slave: int = 1, no_response_expected: b in the response. The entire response message should be identical to the request. """ - return self.execute(no_response_expected, pdu_diag.ReturnQueryDataRequest(msg, dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnQueryDataRequest(msg, dev_id=device_id)) - def diag_restart_communication(self, toggle: bool, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_restart_communication(self, toggle: bool, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose restart communication (code 0x08 sub 0x01). :param toggle: True if toggled. - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -215,24 +215,24 @@ def diag_restart_communication(self, toggle: bool, *, slave: int = 1, no_respons occurs before the restart is update_datastored. """ msg = ModbusStatus.ON if toggle else ModbusStatus.OFF - return self.execute(no_response_expected, pdu_diag.RestartCommunicationsOptionRequest(message=msg, dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.RestartCommunicationsOptionRequest(message=msg, dev_id=device_id)) - def diag_read_diagnostic_register(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_read_diagnostic_register(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose read diagnostic register (code 0x08 sub 0x02). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: The contents of the remote device's 16-bit diagnostic register are returned in the response. """ - return self.execute(no_response_expected, pdu_diag.ReturnDiagnosticRegisterRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnDiagnosticRegisterRequest(dev_id=device_id)) - def diag_change_ascii_input_delimeter(self, *, delimiter: int = 0x0a, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_change_ascii_input_delimeter(self, *, delimiter: int = 0x0a, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose change ASCII input delimiter (code 0x08 sub 0x03). :param delimiter: char to replace LF - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -241,12 +241,12 @@ def diag_change_ascii_input_delimeter(self, *, delimiter: int = 0x0a, slave: int character). This function is useful in cases of a Line Feed is not required at the end of ASCII messages. """ - return self.execute(no_response_expected, pdu_diag.ChangeAsciiInputDelimiterRequest(message=delimiter, dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ChangeAsciiInputDelimiterRequest(message=delimiter, dev_id=device_id)) - def diag_force_listen_only(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_force_listen_only(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose force listen only (code 0x08 sub 0x04). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -256,23 +256,23 @@ def diag_force_listen_only(self, *, slave: int = 1, no_response_expected: bool = allowing them to continue communicating without interruption from the addressed remote device. No response is returned. """ - return self.execute(no_response_expected, pdu_diag.ForceListenOnlyModeRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ForceListenOnlyModeRequest(dev_id=device_id)) - def diag_clear_counters(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_clear_counters(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose clear counters (code 0x08 sub 0x0A). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: Clear ll counters and the diagnostic register. Also, counters are cleared upon power-up """ - return self.execute(no_response_expected, pdu_diag.ClearCountersRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ClearCountersRequest(dev_id=device_id)) - def diag_read_bus_message_count(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_read_bus_message_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose read bus message count (code 0x08 sub 0x0B). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -280,12 +280,12 @@ def diag_read_bus_message_count(self, *, slave: int = 1, no_response_expected: b remote device has detected on the communications systems since its last restart, clear counters operation, or power-up """ - return self.execute(no_response_expected, pdu_diag.ReturnBusMessageCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnBusMessageCountRequest(dev_id=device_id)) - def diag_read_bus_comm_error_count(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_read_bus_comm_error_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -293,12 +293,12 @@ def diag_read_bus_comm_error_count(self, *, slave: int = 1, no_response_expected by the remote device since its last restart, clear counter operation, or power-up """ - return self.execute(no_response_expected, pdu_diag.ReturnBusCommunicationErrorCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnBusCommunicationErrorCountRequest(dev_id=device_id)) - def diag_read_bus_exception_error_count(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_read_bus_exception_error_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -306,12 +306,12 @@ def diag_read_bus_exception_error_count(self, *, slave: int = 1, no_response_exp responses returned by the remote device since its last restart, clear counters operation, or power-up """ - return self.execute(no_response_expected, pdu_diag.ReturnBusExceptionErrorCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnBusExceptionErrorCountRequest(dev_id=device_id)) - def diag_read_slave_message_count(self, *, slave: int = 1, no_response_expected: bool = False) -> T: - """Diagnose read Slave Message Count (code 0x08 sub 0x0E). + def diag_read_device_message_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: + """Diagnose read device Message Count (code 0x08 sub 0x0E). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -319,12 +319,12 @@ def diag_read_slave_message_count(self, *, slave: int = 1, no_response_expected: remote device, that the remote device has processed since its last restart, clear counters operation, or power-up """ - return self.execute(no_response_expected, pdu_diag.ReturnSlaveMessageCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnDeviceMessageCountRequest(dev_id=device_id)) - def diag_read_slave_no_response_count(self, *, slave: int = 1, no_response_expected: bool = False) -> T: - """Diagnose read Slave No Response Count (code 0x08 sub 0x0F). + def diag_read_device_no_response_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: + """Diagnose read device No Response Count (code 0x08 sub 0x0F). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -332,12 +332,12 @@ def diag_read_slave_no_response_count(self, *, slave: int = 1, no_response_expec remote device, that the remote device has processed since its last restart, clear counters operation, or power-up. """ - return self.execute(no_response_expected, pdu_diag.ReturnSlaveNoResponseCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnDeviceNoResponseCountRequest(dev_id=device_id)) - def diag_read_slave_nak_count(self, *, slave: int = 1, no_response_expected: bool = False) -> T: - """Diagnose read Slave NAK Count (code 0x08 sub 0x10). + def diag_read_device_nak_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: + """Diagnose read device NAK Count (code 0x08 sub 0x10). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -346,25 +346,25 @@ def diag_read_slave_nak_count(self, *, slave: int = 1, no_response_expected: boo response, since its last restart, clear counters operation, or power-up. Exception responses are described and listed in section 7 . """ - return self.execute(no_response_expected, pdu_diag.ReturnSlaveNAKCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnDeviceNAKCountRequest(dev_id=device_id)) - def diag_read_slave_busy_count(self, *, slave: int = 1, no_response_expected: bool = False) -> T: - """Diagnose read Slave Busy Count (code 0x08 sub 0x11). + def diag_read_device_busy_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: + """Diagnose read device Busy Count (code 0x08 sub 0x11). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: The response data field returns the quantity of messages addressed to the - remote device for which it returned a Slave Device Busy exception response, + remote device for which it returned device Busy exception response, since its last restart, clear counters operation, or power-up. """ - return self.execute(no_response_expected, pdu_diag.ReturnSlaveBusyCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnDeviceBusyCountRequest(dev_id=device_id)) - def diag_read_bus_char_overrun_count(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_read_bus_char_overrun_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -374,12 +374,12 @@ def diag_read_bus_char_overrun_count(self, *, slave: int = 1, no_response_expect overrun is caused by data characters arriving at the port faster than they can be stored, or by the loss of a character due to a hardware malfunction. """ - return self.execute(no_response_expected, pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnDeviceBusCharacterOverrunCountRequest(dev_id=device_id)) - def diag_read_iop_overrun_count(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_read_iop_overrun_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Iop overrun count (code 0x08 sub 0x13). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -387,25 +387,25 @@ def diag_read_iop_overrun_count(self, *, slave: int = 1, no_response_expected: b faster than they can be stored, or by the loss of a character due to a hardware malfunction. This function is specific to the 884. """ - return self.execute(no_response_expected, pdu_diag.ReturnIopOverrunCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnIopOverrunCountRequest(dev_id=device_id)) - def diag_clear_overrun_counter(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_clear_overrun_counter(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: An error flag should be cleared, but nothing else in the specification mentions is, so it is ignored. """ - return self.execute(no_response_expected, pdu_diag.ClearOverrunCountRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.ClearOverrunCountRequest(dev_id=device_id)) - def diag_getclear_modbus_response(self, *, data: int = 0, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_getclear_modbus_response(self, *, data: int = 0, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose Get/Clear modbus plus (code 0x08 sub 0x15). :param data: "Get Statistics" or "Clear Statistics" - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -415,15 +415,14 @@ def diag_getclear_modbus_response(self, *, data: int = 0, slave: int = 1, no_res operation. The two operations are exclusive - the "Get" operation cannot clear the statistics, and the "Clear" operation does not return statistics prior to clearing - them. Statistics are also cleared on power-up of the slave - device. + them. Statistics are also cleared on power-up of the device, """ - return self.execute(no_response_expected, pdu_diag.GetClearModbusPlusRequest(message=data, dev_id=slave)) + return self.execute(no_response_expected, pdu_diag.GetClearModbusPlusRequest(message=data, dev_id=device_id)) - def diag_get_comm_event_counter(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_get_comm_event_counter(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose get event counter (code 0x0B). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -440,12 +439,12 @@ def diag_get_comm_event_counter(self, *, slave: int = 1, no_response_expected: b The event counter can be reset by means of the Diagnostics function Restart Communications or Clear Counters and Diagnostic Register. """ - return self.execute(no_response_expected, pdu_other_msg.GetCommEventCounterRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_other_msg.GetCommEventCounterRequest(dev_id=device_id)) - def diag_get_comm_event_log(self, *, slave: int = 1, no_response_expected: bool = False) -> T: + def diag_get_comm_event_log(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: """Diagnose get event counter (code 0x0C). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -467,21 +466,21 @@ def diag_get_comm_event_log(self, *, slave: int = 1, no_response_expected: bool chronological order. Byte 0 is the most recent event. Each new byte flushes the oldest byte from the field. """ - return self.execute(no_response_expected, pdu_other_msg.GetCommEventLogRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_other_msg.GetCommEventLogRequest(dev_id=device_id)) def write_coils( self, address: int, values: list[bool], *, - slave: int = 1, + device_id: int = 1, no_response_expected: bool = False ) -> T: """Write coils (code 0x0F). :param address: Start address to write to :param values: List of booleans to write, or a single boolean to write - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -489,7 +488,7 @@ def write_coils( Coils are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0). """ - pdu = pdu_bit.WriteMultipleCoilsRequest(address=address, bits=values, dev_id=slave) + pdu = pdu_bit.WriteMultipleCoilsRequest(address=address, bits=values, dev_id=device_id) return self.execute(no_response_expected, pdu) def write_registers( @@ -497,39 +496,39 @@ def write_registers( address: int, values: list[int], *, - slave: int = 1, + device_id: int = 1, no_response_expected: bool = False ) -> T: """Write registers (code 0x10). :param address: Start address to write to :param values: List of values to write - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: This function is used to write a block of contiguous registers (1 to approx. 120 registers) in a remote device. """ - return self.execute(no_response_expected, pdu_reg.WriteMultipleRegistersRequest(address=address, registers=values,dev_id=slave)) + return self.execute(no_response_expected, pdu_reg.WriteMultipleRegistersRequest(address=address, registers=values,dev_id=device_id)) - def report_slave_id(self, *, slave: int = 1, no_response_expected: bool = False) -> T: - """Report slave ID (code 0x11). + def report_device_id(self, *, device_id: int = 1, no_response_expected: bool = False) -> T: + """Report device ID (code 0x11). - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: This function is used to read the description of the type, the current status and other information specific to a remote device. """ - return self.execute(no_response_expected, pdu_other_msg.ReportSlaveIdRequest(dev_id=slave)) + return self.execute(no_response_expected, pdu_other_msg.ReportDeviceIdRequest(dev_id=device_id)) - def read_file_record(self, records: list[pdu_file_msg.FileRecord], *, slave: int = 1, no_response_expected: bool = False) -> T: + def read_file_record(self, records: list[pdu_file_msg.FileRecord], *, device_id: int = 1, no_response_expected: bool = False) -> T: """Read file record (code 0x14). :param records: List of FileRecord (Reference type, File number, Record Number) - :param slave: device id + :param device_id: device id :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -553,13 +552,13 @@ def read_file_record(self, records: list[pdu_file_msg.FileRecord], *, slave: int in the expected response, must not exceed the allowable length of the MODBUS PDU: 235 bytes. """ - return self.execute(no_response_expected, pdu_file_msg.ReadFileRecordRequest(records, dev_id=slave)) + return self.execute(no_response_expected, pdu_file_msg.ReadFileRecordRequest(records, dev_id=device_id)) - def write_file_record(self, records: list[pdu_file_msg.FileRecord], *, slave: int = 1, no_response_expected: bool = False) -> T: + def write_file_record(self, records: list[pdu_file_msg.FileRecord], *, device_id: int = 1, no_response_expected: bool = False) -> T: """Write file record (code 0x15). :param records: List of File_record (Reference type, File number, Record Number, Record Length, Record Data) - :param slave: (optional) Device id + :param device_id: (optional) Device id :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -568,7 +567,7 @@ def write_file_record(self, records: list[pdu_file_msg.FileRecord], *, slave: in and all record lengths are provided in terms of the number of 16 bit words. """ - return self.execute(no_response_expected, pdu_file_msg.WriteFileRecordRequest(records=records, dev_id=slave)) + return self.execute(no_response_expected, pdu_file_msg.WriteFileRecordRequest(records=records, dev_id=device_id)) def mask_write_register( self, @@ -576,7 +575,7 @@ def mask_write_register( address: int = 0x0000, and_mask: int = 0xFFFF, or_mask: int = 0x0000, - slave: int = 1, + device_id: int = 1, no_response_expected: bool = False ) -> T: """Mask write register (code 0x16). @@ -584,7 +583,7 @@ def mask_write_register( :param address: The mask pointer address (0x0000 to 0xffff) :param and_mask: The and bitmask to apply to the register address :param or_mask: The or bitmask to apply to the register address - :param slave: (optional) device id + :param device_id: (optional) device id :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -593,7 +592,7 @@ def mask_write_register( The function can be used to set or clear individual bits in the register. """ - return self.execute(no_response_expected, pdu_reg.MaskWriteRegisterRequest(address=address, and_mask=and_mask, or_mask=or_mask, dev_id=slave)) + return self.execute(no_response_expected, pdu_reg.MaskWriteRegisterRequest(address=address, and_mask=and_mask, or_mask=or_mask, dev_id=device_id)) def readwrite_registers( self, @@ -603,7 +602,7 @@ def readwrite_registers( write_address: int = 0, address: int | None = None, values: list[int] | None = None, - slave: int = 1, + device_id: int = 1, no_response_expected: bool = False ) -> T: """Read/Write registers (code 0x17). @@ -613,7 +612,7 @@ def readwrite_registers( :param write_address: The address to start writing to :param address: (optional) use as read/write address :param values: List of values to write, or a single value to write - :param slave: (optional) Modbus slave ID + :param device_id: (optional) Modbus device ID :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -629,13 +628,13 @@ def readwrite_registers( if address: read_address = address write_address = address - return self.execute(no_response_expected, pdu_reg.ReadWriteMultipleRegistersRequest( read_address=read_address, read_count=read_count, write_address=write_address, write_registers=values,dev_id=slave)) + return self.execute(no_response_expected, pdu_reg.ReadWriteMultipleRegistersRequest( read_address=read_address, read_count=read_count, write_address=write_address, write_registers=values,dev_id=device_id)) - def read_fifo_queue(self, *, address: int = 0x0000, slave: int = 1, no_response_expected: bool = False) -> T: + def read_fifo_queue(self, *, address: int = 0x0000, device_id: int = 1, no_response_expected: bool = False) -> T: """Read FIFO queue (code 0x18). :param address: The address to start reading from - :param slave: (optional) device id + :param device_id: (optional) device id :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -649,19 +648,19 @@ def read_fifo_queue(self, *, address: int = 0x0000, slave: int = 1, no_response_ registers. The function reads the queue contents, but does not clear them. """ - return self.execute(no_response_expected, pdu_file_msg.ReadFifoQueueRequest(address, dev_id=slave)) + return self.execute(no_response_expected, pdu_file_msg.ReadFifoQueueRequest(address, dev_id=device_id)) # code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED def read_device_information(self, *, read_code: int | None = None, object_id: int = 0x00, - slave: int = 1, + device_id: int = 1, no_response_expected: bool = False) -> T: """Read FIFO queue (code 0x2B sub 0x0E). :param read_code: The device information read code :param object_id: The object to read from - :param slave: (optional) Device id + :param device_id: (optional) Device id :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: @@ -673,7 +672,7 @@ def read_device_information(self, *, read_code: int | None = None, composed of a set of addressable data elements. The data elements are called objects and an object Id identifies them. """ - return self.execute(no_response_expected, pdu_mei.ReadDeviceInformationRequest(read_code, object_id, dev_id=slave)) + return self.execute(no_response_expected, pdu_mei.ReadDeviceInformationRequest(read_code, object_id, dev_id=device_id)) # ------------------ # Converter methods diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index 633fb3f16..fb08076bd 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -270,7 +270,7 @@ def recv(self, size: int | None) -> bytes: break # Timeout is reduced also if some data has been received in order # to avoid infinite loops when there isn't an expected response - # size and the slave sends noisy data continuously. + # size and the device sends noisy data continuously. if time_ > end: break @@ -306,7 +306,7 @@ def _handle_abrupt_socket_close(self, size: int | None, data: list[bytes], durat result = b"".join(data) Log.warning(" after returning {} bytes: {} ", len(result), result) return result - msg += " without response from slave before it closed connection" + msg += " without response from device before it closed connection" raise ConnectionException(msg) def is_socket_open(self) -> bool: diff --git a/pymodbus/constants.py b/pymodbus/constants.py index 446defe9a..472c70e4c 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -29,22 +29,12 @@ class ModbusStatus(int, enum.Enum): .. attribute:: OFF This indicates that the given modbus entity is off - - .. attribute:: SLAVE_ON - - This indicates that the given modbus slave is running - - .. attribute:: SLAVE_OFF - - This indicates that the given modbus slave is not running """ WAITING = 0xFFFF READY = 0x0000 ON = 0xFF00 OFF = 0x0000 - SLAVE_ON = 0xFF - SLAVE_OFF = 0x00 class Endian(str, enum.Enum): diff --git a/pymodbus/datastore/__init__.py b/pymodbus/datastore/__init__.py index abd350601..ac000986a 100644 --- a/pymodbus/datastore/__init__.py +++ b/pymodbus/datastore/__init__.py @@ -1,18 +1,18 @@ """Datastore.""" __all__ = [ - "ModbusBaseSlaveContext", + "ModbusBaseDeviceContext", + "ModbusDeviceContext", "ModbusSequentialDataBlock", "ModbusServerContext", "ModbusSimulatorContext", - "ModbusSlaveContext", "ModbusSparseDataBlock", ] from pymodbus.datastore.context import ( - ModbusBaseSlaveContext, + ModbusBaseDeviceContext, + ModbusDeviceContext, ModbusServerContext, - ModbusSlaveContext, ) from pymodbus.datastore.simulator import ModbusSimulatorContext from pymodbus.datastore.store import ( diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index e18b268bb..c8b624916 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -6,12 +6,12 @@ # pylint: disable=missing-type-doc from pymodbus.datastore.store import ModbusSequentialDataBlock -from pymodbus.exceptions import NoSuchSlaveException +from pymodbus.exceptions import NoSuchIdException from pymodbus.logging import Log -class ModbusBaseSlaveContext: - """Interface for a modbus slave data context. +class ModbusBaseDeviceContext: + """Interface for a modbus device data context. Derived classes must implemented the following methods: reset(self) @@ -71,9 +71,9 @@ def setValues(self, fc_as_hex: int, address: int, values: Sequence[int | bool]) # ---------------------------------------------------------------------------# -# Slave Contexts +# Device Contexts # ---------------------------------------------------------------------------# -class ModbusSlaveContext(ModbusBaseSlaveContext): +class ModbusDeviceContext(ModbusBaseDeviceContext): """Create a modbus data model with data stored in a block. :param di: discrete inputs initializer ModbusDataBlock @@ -100,7 +100,7 @@ def __str__(self): :returns: A string representation of the context """ - return "Modbus Slave Context" + return "Modbus device Context" def reset(self): """Reset all the datastores to their default values.""" @@ -131,7 +131,7 @@ def setValues(self, fc_as_hex, address, values): self.store[self.decode(fc_as_hex)].setValues(address, values) def register(self, function_code, fc_as_hex, datablock=None): - """Register a datablock with the slave context. + """Register a datablock with the device context. :param function_code: function code (int) :param fc_as_hex: string representation of function code (e.g "cf" ) @@ -142,83 +142,82 @@ def register(self, function_code, fc_as_hex, datablock=None): class ModbusServerContext: - """This represents a master collection of slave contexts. + """This represents a master collection of device contexts. If single is set to true, it will be treated as a single context so every device id returns the same context. If single is set to false, it will be interpreted as a collection of - slave contexts. + device contexts. """ - def __init__(self, slaves=None, single=True): + def __init__(self, devices=None, single=True): """Initialize a new instance of a modbus server context. - :param slaves: A dictionary of client contexts + :param devices: A dictionary of client contexts :param single: Set to true to treat this as a single context """ self.single = single - self._slaves = slaves or {} + self._devices = devices or {} if self.single: - self._slaves = {0: self._slaves} + self._devices = {0: self._devices} def __iter__(self): - """Iterate over the current collection of slave contexts. + """Iterate over the current collection of device contexts. - :returns: An iterator over the slave contexts + :returns: An iterator over the device contexts """ - return iter(self._slaves.items()) + return iter(self._devices.items()) - def __contains__(self, slave): - """Check if the given slave is in this list. + def __contains__(self, device_id): + """Check if the given device_id is in this list. - :param slave: slave The slave to check for existence - :returns: True if the slave exists, False otherwise + :param device_id: device_id The device_id to check for existence + :returns: True if the device_id exists, False otherwise """ - if self.single and self._slaves: + if self.single and self._devices: return True - return slave in self._slaves + return device_id in self._devices - def __setitem__(self, slave, context): - """Use to set a new slave context. + def __setitem__(self, device_id, context): + """Use to set a new device_id context. - :param slave: The slave context to set - :param context: The new context to set for this slave - :raises NoSuchSlaveException: + :param device_id: The device_id context to set + :param context: The new context to set for this device_id + :raises NoSuchIdException: """ if self.single: - slave = 0 - if 0xF7 >= slave >= 0x00: - self._slaves[slave] = context + device_id = 0 + if 0xF7 >= device_id >= 0x00: + self._devices[device_id] = context else: - raise NoSuchSlaveException(f"slave index :{slave} out of range") + raise NoSuchIdException(f"device_id index :{device_id} out of range") - def __delitem__(self, slave): - """Use to access the slave context. + def __delitem__(self, device_id): + """Use to access the device_id context. - :param slave: The slave context to remove - :raises NoSuchSlaveException: + :param device_id: The device context to remove + :raises NoSuchIdException: """ - if not self.single and (0xF7 >= slave >= 0x00): - del self._slaves[slave] + if not self.single and (0xF7 >= device_id >= 0x00): + del self._devices[device_id] else: - raise NoSuchSlaveException(f"slave index: {slave} out of range") + raise NoSuchIdException(f"device_id index: {device_id} out of range") - def __getitem__(self, slave): - """Use to get access to a slave context. + def __getitem__(self, device_id): + """Use to get access to a device_id context. - :param slave: The slave context to get - :returns: The requested slave context - :raises NoSuchSlaveException: + :param device_id: The device context to get + :returns: The requested device context + :raises NoSuchIdException: """ if self.single: - slave = 0 - if slave in self._slaves: - return self._slaves.get(slave) - raise NoSuchSlaveException( - f"slave - {slave} does not exist, or is out of range" + device_id = 0 + if device_id in self._devices: + return self._devices.get(device_id) + raise NoSuchIdException( + f"device_id - {device_id} does not exist, or is out of range" ) - def slaves(self): - """Define slaves.""" - # Python3 now returns keys() as iterable - return list(self._slaves.keys()) + def device_ids(self): + """Define device_ids.""" + return list(self._devices.keys()) diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index ffd6833eb..02d0ebf08 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -1,5 +1,5 @@ """Remote datastore.""" -from pymodbus.datastore import ModbusBaseSlaveContext +from pymodbus.datastore import ModbusBaseDeviceContext from pymodbus.exceptions import NotImplementedException from pymodbus.logging import Log @@ -7,21 +7,21 @@ # ---------------------------------------------------------------------------# # Context # ---------------------------------------------------------------------------# -class RemoteSlaveContext(ModbusBaseSlaveContext): +class RemoteDeviceContext(ModbusBaseDeviceContext): """TODO. This creates a modbus data model that connects to a remote device (depending on the client used) """ - def __init__(self, client, slave=None): + def __init__(self, client, device_id=None): """Initialize the datastores. :param client: The client to retrieve values with - :param slave: Unit ID of the remote slave + :param device_id: Unit ID of the remote device """ self._client = client - self.slave = slave + self.device_id = device_id self.result = None self.__build_mapping() if not self.__set_callbacks: @@ -58,13 +58,13 @@ def __str__(self): :returns: A string representation of the context """ - return f"Remote Slave Context({self._client})" + return f"Remote Device Context({self._client})" def __build_mapping(self): """Build the function code mapper.""" params = {} - if self.slave: - params["slave"] = self.slave + if self.device_id: + params["device_id"] = self.device_id self.__get_callbacks = { "d": lambda a, c: self._client.read_discrete_inputs( a, count=c, **params diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py index fd3802604..42e7ecb02 100644 --- a/pymodbus/datastore/simulator.py +++ b/pymodbus/datastore/simulator.py @@ -8,7 +8,7 @@ from datetime import datetime from typing import Any -from pymodbus.datastore.context import ModbusBaseSlaveContext +from pymodbus.datastore.context import ModbusBaseDeviceContext WORD_SIZE = 16 @@ -373,7 +373,7 @@ def setup(self, config, custom_actions) -> None: raise RuntimeError(f"INVALID key in setup: {self.config}") -class ModbusSimulatorContext(ModbusBaseSlaveContext): +class ModbusSimulatorContext(ModbusBaseDeviceContext): """Modbus simulator. :param config: A dict with structure as shown below. diff --git a/pymodbus/exceptions.py b/pymodbus/exceptions.py index 7c685879b..89e219d02 100644 --- a/pymodbus/exceptions.py +++ b/pymodbus/exceptions.py @@ -8,7 +8,7 @@ "InvalidMessageReceivedException", "MessageRegisterException", "ModbusIOException", - "NoSuchSlaveException", + "NoSuchIdException", "NotImplementedException", "ParameterException", ] @@ -59,15 +59,15 @@ def __init__(self, string=""): ModbusException.__init__(self, message) -class NoSuchSlaveException(ModbusException): - """Error resulting from making a request to a slave that does not exist.""" +class NoSuchIdException(ModbusException): + """Error resulting from making a request to a id that does not exist.""" def __init__(self, string=""): """Initialize the exception. :param string: The message to append to the error """ - message = f"[No Such Slave] {string}" + message = f"[No Such id] {string}" ModbusException.__init__(self, message) diff --git a/pymodbus/framer/rtu.py b/pymodbus/framer/rtu.py index 77321ec20..ebe130b9a 100644 --- a/pymodbus/framer/rtu.py +++ b/pymodbus/framer/rtu.py @@ -24,25 +24,25 @@ class FramerRTU(FramerBase): For client: - a request causes 1 response ! - - Multiple requests are NOT allowed (master-slave protocol) + - Multiple requests are NOT allowed (master controlled protocol) - the server will not retransmit responses this means decoding is always exactly 1 frame (response) For server (Single device) - - only 1 request allowed (master-slave) protocol + - only 1 request allowed (master controlled protocol) - the client (master) may retransmit but in larger time intervals this means decoding is always exactly 1 frame (request) For server (Multidrop line --> devices in parallel) - - only 1 request allowed (master-slave) protocol + - only 1 request allowed (master controlled protocol) - other devices will send responses - the client (master) may retransmit but in larger time intervals this means decoding is always exactly 1 frame request, however some requests - will be for unknown slaves, which must be ignored together with the - response from the unknown slave. + will be for unknown devices, which must be ignored together with the + response from the unknown device. Recovery from bad cabling and unstable USB etc is important, the following scenarios is possible: @@ -78,7 +78,7 @@ class FramerRTU(FramerBase): """ - MIN_SIZE = 4 # + MIN_SIZE = 4 # @classmethod def generate_crc16_table(cls) -> list[int]: diff --git a/pymodbus/pdu/bit_message.py b/pymodbus/pdu/bit_message.py index 723ac6e10..5bf3cacad 100644 --- a/pymodbus/pdu/bit_message.py +++ b/pymodbus/pdu/bit_message.py @@ -4,7 +4,7 @@ from typing import cast from pymodbus.constants import ModbusStatus -from pymodbus.datastore import ModbusSlaveContext +from pymodbus.datastore import ModbusDeviceContext from pymodbus.pdu.pdu import ModbusPDU from pymodbus.utilities import pack_bitstring, unpack_bitstring @@ -33,7 +33,7 @@ def get_response_pdu_size(self) -> int: """ return 1 + 1 + (self.count + 7) // 8 - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Run request against a datastore.""" values = await context.async_getValues( self.function_code, self.address, self.count @@ -91,7 +91,7 @@ def decode(self, data: bytes) -> None: class WriteSingleCoilRequest(WriteSingleCoilResponse): """WriteSingleCoilRequest.""" - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Run a request against a datastore.""" await context.async_setValues(self.function_code, self.address, self.bits) values = cast(list[bool], await context.async_getValues(self.function_code, self.address, 1)) @@ -124,7 +124,7 @@ def decode(self, data: bytes) -> None: self.address, count, _byte_count = struct.unpack(">HHB", data[0:5]) self.bits = unpack_bitstring(data[5:])[:count] - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Run a request against a datastore.""" count = len(self.bits) await context.async_setValues( diff --git a/pymodbus/pdu/decoders.py b/pymodbus/pdu/decoders.py index 27a4d5d42..606534d2a 100644 --- a/pymodbus/pdu/decoders.py +++ b/pymodbus/pdu/decoders.py @@ -29,7 +29,7 @@ class DecodePDU: (o_msg.ReadExceptionStatusRequest, o_msg.ReadExceptionStatusResponse), (o_msg.GetCommEventCounterRequest, o_msg.GetCommEventCounterResponse), (o_msg.GetCommEventLogRequest, o_msg.GetCommEventLogResponse), - (o_msg.ReportSlaveIdRequest, o_msg.ReportSlaveIdResponse), + (o_msg.ReportDeviceIdRequest, o_msg.ReportDeviceIdResponse), (file_msg.ReadFileRecordRequest, file_msg.ReadFileRecordResponse), (file_msg.WriteFileRecordRequest, file_msg.WriteFileRecordResponse), (reg_msg.MaskWriteRegisterRequest, reg_msg.MaskWriteRegisterResponse), @@ -47,11 +47,11 @@ class DecodePDU: (diag_msg.ReturnBusMessageCountRequest, diag_msg.ReturnBusMessageCountResponse), (diag_msg.ReturnBusCommunicationErrorCountRequest, diag_msg.ReturnBusCommunicationErrorCountResponse), (diag_msg.ReturnBusExceptionErrorCountRequest, diag_msg.ReturnBusExceptionErrorCountResponse), - (diag_msg.ReturnSlaveMessageCountRequest, diag_msg.ReturnSlaveMessageCountResponse), - (diag_msg.ReturnSlaveNoResponseCountRequest, diag_msg.ReturnSlaveNoResponseCountResponse), - (diag_msg.ReturnSlaveNAKCountRequest, diag_msg.ReturnSlaveNAKCountResponse), - (diag_msg.ReturnSlaveBusyCountRequest, diag_msg.ReturnSlaveBusyCountResponse), - (diag_msg.ReturnSlaveBusCharacterOverrunCountRequest, diag_msg.ReturnSlaveBusCharacterOverrunCountResponse), + (diag_msg.ReturnDeviceMessageCountRequest, diag_msg.ReturnDeviceMessageCountResponse), + (diag_msg.ReturnDeviceNoResponseCountRequest, diag_msg.ReturnDeviceNoResponseCountResponse), + (diag_msg.ReturnDeviceNAKCountRequest, diag_msg.ReturnDeviceNAKCountResponse), + (diag_msg.ReturnDeviceBusyCountRequest, diag_msg.ReturnDeviceBusyCountResponse), + (diag_msg.ReturnDeviceBusCharacterOverrunCountRequest, diag_msg.ReturnDeviceBusCharacterOverrunCountResponse), (diag_msg.ReturnIopOverrunCountRequest, diag_msg.ReturnIopOverrunCountResponse), (diag_msg.ClearOverrunCountRequest, diag_msg.ClearOverrunCountResponse), (diag_msg.GetClearModbusPlusRequest, diag_msg.GetClearModbusPlusResponse), diff --git a/pymodbus/device.py b/pymodbus/pdu/device.py similarity index 94% rename from pymodbus/device.py rename to pymodbus/pdu/device.py index 541369905..51c9a0213 100644 --- a/pymodbus/device.py +++ b/pymodbus/pdu/device.py @@ -19,7 +19,7 @@ from collections import OrderedDict from pymodbus.constants import INTERNAL_ERROR, DeviceInformation -from pymodbus.events import ModbusEvent +from pymodbus.pdu.events import ModbusEvent from pymodbus.utilities import dict_property @@ -46,13 +46,13 @@ class ModbusPlusStatistics: "data_master_token_failed": [0x00], # 07 lo "program_master_token_owner": [0x00], # 08 hi "data_master_token_owner": [0x00], # 08 lo - "program_slave_token_owner": [0x00], # 09 hi - "data_slave_token_owner": [0x00], # 09 lo - "data_slave_command_transfer": [0x00], # 10 hi + "program_id_token_owner": [0x00], # 09 hi + "data_id_token_owner": [0x00], # 09 lo + "data_id_command_transfer": [0x00], # 10 hi "__unused_10_lowbit": [0x00], # 10 lo - "program_slave_command_transfer": [0x00], # 11 hi + "program_id_command_transfer": [0x00], # 11 hi "program_master_rsp_transfer": [0x00], # 11 lo - "program_slave_auto_logout": [0x00], # 12 hi + "program_id_auto_logout": [0x00], # 12 hi "program_master_connect_status": [0x00], # 12 lo "receive_buffer_dma_overrun": [0x00], # 13 hi "pretransmit_deferral_error": [0x00], # 13 lo @@ -79,9 +79,9 @@ class ModbusPlusStatistics: "global_data_bit_map": [0x00] * 8, # 31-34 "receive_buffer_use_bit_map": [0x00] * 8, # 35-37 "data_master_output_path": [0x00] * 8, # 38-41 - "data_slave_input_path": [0x00] * 8, # 42-45 + "data_id_input_path": [0x00] * 8, # 42-45 "program_master_outptu_path": [0x00] * 8, # 46-49 - "program_slave_input_path": [0x00] * 8, # 50-53 + "program_id_input_path": [0x00] * 8, # 50-53 } ) @@ -320,27 +320,27 @@ class ModbusCountersHandler: not able to calculate the CRC. In such cases, this counter is also incremented. - 0x0D 3 Return Slave Exception Error Count + 0x0D 3 Return device Exception Error Count Quantity of MODBUS exception error detected by the remote device since its last restart, clear counters operation, or power-up. Exception errors are described and listed in "MODBUS Application Protocol Specification" document. - 0xOE 4 Return Slave Message Count + 0xOE 4 Return device Message Count Quantity of messages addressed to the remote device that the remote device has processed since its last restart, clear counters operation, or power-up. - 0x0F 5 Return Slave No Response Count + 0x0F 5 Return device No Response Count Quantity of messages received by the remote device for which it returned no response (neither a normal response nor an exception response), since its last restart, clear counters operation, or power-up. - 0x10 6 Return Slave NAK Count + 0x10 6 Return device NAK Count Quantity of messages addressed to the remote device for which it returned a Negative ACKNOWLEDGE (NAK) exception response, since @@ -348,10 +348,10 @@ class ModbusCountersHandler: responses are described and listed in "MODBUS Application Protocol Specification" document. - 0x11 7 Return Slave Busy Count + 0x11 7 Return device Busy Count Quantity of messages addressed to the remote device for which it - returned a Slave Device Busy exception response, since its last + returned a device Device Busy exception response, since its last restart, clear counters operation, or power-up. Exception responses are described and listed in "MODBUS Application Protocol Specification" document. @@ -371,11 +371,11 @@ class ModbusCountersHandler: __names = [ "BusMessage", "BusCommunicationError", - "SlaveExceptionError", - "SlaveMessage", - "SlaveNoResponse", - "SlaveNAK", - "SLAVE_BUSY", + "DeviceExceptionError", + "DeviceMessage", + "DeviceNoResponse", + "DeviceeNAK", + "DEVICE_BUSY", "BusCharacterOverrun", ] @@ -422,10 +422,10 @@ def summary(self): BusMessage = dict_property(lambda s: s.__data, 0) # pylint: disable=protected-access BusCommunicationError = dict_property(lambda s: s.__data, 1) # pylint: disable=protected-access BusExceptionError = dict_property(lambda s: s.__data, 2) # pylint: disable=protected-access - SlaveMessage = dict_property(lambda s: s.__data, 3) # pylint: disable=protected-access - SlaveNoResponse = dict_property(lambda s: s.__data, 4) # pylint: disable=protected-access - SlaveNAK = dict_property(lambda s: s.__data, 5) # pylint: disable=protected-access - SLAVE_BUSY = dict_property(lambda s: s.__data, 6) # pylint: disable=protected-access + DeviceMessage = dict_property(lambda s: s.__data, 3) # pylint: disable=protected-access + DeviceNoResponse = dict_property(lambda s: s.__data, 4) # pylint: disable=protected-access + DeviceNAK = dict_property(lambda s: s.__data, 5) # pylint: disable=protected-access + DEVICE_BUSY = dict_property(lambda s: s.__data, 6) # pylint: disable=protected-access BusCharacterOverrun = dict_property(lambda s: s.__data, 7) # pylint: disable=protected-access Event = dict_property(lambda s: s.__data, 8) # pylint: disable=protected-access # fmt: on diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index 674f367cd..1534c39a6 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -5,8 +5,8 @@ from typing import cast from pymodbus.constants import ModbusPlusOperation -from pymodbus.datastore import ModbusSlaveContext -from pymodbus.device import ModbusControlBlock +from pymodbus.datastore import ModbusDeviceContext +from pymodbus.pdu.device import ModbusControlBlock from pymodbus.pdu.pdu import ModbusPDU from pymodbus.utilities import pack_bitstring @@ -65,7 +65,7 @@ def get_response_pdu_size(self) -> int: """ return 1 + 2 + 2 - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """Implement dummy.""" response = { DiagnosticBase.sub_function_code: DiagnosticBase, @@ -104,7 +104,7 @@ class ReturnDiagnosticRegisterRequest(DiagnosticBase): sub_function_code = 0x0002 - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" register = pack_bitstring(_MCB.getDiagnosticRegister()) return ReturnDiagnosticRegisterResponse(message=register, dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -121,7 +121,7 @@ class ChangeAsciiInputDelimiterRequest(DiagnosticBase): sub_function_code = 0x0003 - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" char = (cast(int, self.message) & 0xFF00) >> 8 _MCB.Delimiter = char @@ -139,7 +139,7 @@ class ForceListenOnlyModeRequest(DiagnosticBase): sub_function_code = 0x0004 - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" _MCB.ListenOnly = True return ForceListenOnlyModeResponse(dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -164,7 +164,7 @@ class ClearCountersRequest(DiagnosticBase): sub_function_code = 0x000A - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" _MCB.reset() return ClearCountersResponse(dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -181,7 +181,7 @@ class ReturnBusMessageCountRequest(DiagnosticBase): sub_function_code = 0x000B - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" count = _MCB.Counter.BusMessage return ReturnBusMessageCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -198,7 +198,7 @@ class ReturnBusCommunicationErrorCountRequest(DiagnosticBase): sub_function_code = 0x000C - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" count = _MCB.Counter.BusCommunicationError return ReturnBusCommunicationErrorCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -215,7 +215,7 @@ class ReturnBusExceptionErrorCountRequest(DiagnosticBase): sub_function_code = 0x000D - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" count = _MCB.Counter.BusExceptionError return ReturnBusExceptionErrorCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -227,87 +227,87 @@ class ReturnBusExceptionErrorCountResponse(DiagnosticBase): sub_function_code = 0x000D -class ReturnSlaveMessageCountRequest(DiagnosticBase): - """ReturnSlaveMessageCountRequest.""" +class ReturnDeviceMessageCountRequest(DiagnosticBase): + """ReturnDeviceMessageCountRequest.""" sub_function_code = 0x000E - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" - count = _MCB.Counter.SlaveMessage - return ReturnSlaveMessageCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) + count = _MCB.Counter.DeviceMessage + return ReturnDeviceMessageCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) -class ReturnSlaveMessageCountResponse(DiagnosticBase): - """ReturnSlaveMessageCountResponse.""" +class ReturnDeviceMessageCountResponse(DiagnosticBase): + """ReturnDeviceMessageCountResponse.""" sub_function_code = 0x000E -class ReturnSlaveNoResponseCountRequest(DiagnosticBase): - """ReturnSlaveNoResponseCountRequest.""" +class ReturnDeviceNoResponseCountRequest(DiagnosticBase): + """ReturnDeviceNoResponseCountRequest.""" sub_function_code = 0x000F - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" - count = _MCB.Counter.SlaveNoResponse - return ReturnSlaveNoResponseCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) + count = _MCB.Counter.DeviceNoResponse + return ReturnDeviceNoResponseCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) -class ReturnSlaveNoResponseCountResponse(DiagnosticBase): - """ReturnSlaveNoResponseCountResponse.""" +class ReturnDeviceNoResponseCountResponse(DiagnosticBase): + """ReturnDeviceNoResponseCountResponse.""" sub_function_code = 0x000F -class ReturnSlaveNAKCountRequest(DiagnosticBase): - """ReturnSlaveNAKCountRequest.""" +class ReturnDeviceNAKCountRequest(DiagnosticBase): + """ReturnDeviceNAKCountRequest.""" sub_function_code = 0x0010 - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" - count = _MCB.Counter.SlaveNAK - return ReturnSlaveNAKCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) + count = _MCB.Counter.DeviceNAK + return ReturnDeviceNAKCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) -class ReturnSlaveNAKCountResponse(DiagnosticBase): - """ReturnSlaveNAKCountResponse.""" +class ReturnDeviceNAKCountResponse(DiagnosticBase): + """ReturnDeviceNAKCountResponse.""" sub_function_code = 0x0010 -class ReturnSlaveBusyCountRequest(DiagnosticBase): - """ReturnSlaveBusyCountRequest.""" +class ReturnDeviceBusyCountRequest(DiagnosticBase): + """ReturnDeviceBusyCountRequest.""" sub_function_code = 0x0011 - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" - count = _MCB.Counter.SLAVE_BUSY - return ReturnSlaveBusyCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) + count = _MCB.Counter.DEVICE_BUSY + return ReturnDeviceBusyCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) -class ReturnSlaveBusyCountResponse(DiagnosticBase): - """ReturnSlaveBusyCountResponse.""" +class ReturnDeviceBusyCountResponse(DiagnosticBase): + """ReturnDeviceBusyCountResponse.""" sub_function_code = 0x0011 -class ReturnSlaveBusCharacterOverrunCountRequest(DiagnosticBase): - """ReturnSlaveBusCharacterOverrunCountRequest.""" +class ReturnDeviceBusCharacterOverrunCountRequest(DiagnosticBase): + """ReturnDeviceBusCharacterOverrunCountRequest.""" sub_function_code = 0x0012 - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" count = _MCB.Counter.BusCharacterOverrun - return ReturnSlaveBusCharacterOverrunCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) + return ReturnDeviceBusCharacterOverrunCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) -class ReturnSlaveBusCharacterOverrunCountResponse(DiagnosticBase): - """ReturnSlaveBusCharacterOverrunCountResponse.""" +class ReturnDeviceBusCharacterOverrunCountResponse(DiagnosticBase): + """ReturnDeviceBusCharacterOverrunCountResponse.""" sub_function_code = 0x0012 @@ -317,7 +317,7 @@ class ReturnIopOverrunCountRequest(DiagnosticBase): sub_function_code = 0x0013 - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" count = _MCB.Counter.BusCharacterOverrun return ReturnIopOverrunCountResponse(message=count, dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -334,7 +334,7 @@ class ClearOverrunCountRequest(DiagnosticBase): sub_function_code = 0x0014 - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" _MCB.Counter.BusCharacterOverrun = 0x0000 return ClearOverrunCountResponse(dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -359,7 +359,7 @@ def get_response_pdu_size(self): data = 2 + 108 if self.message == ModbusPlusOperation.GET_STATISTICS else 0 return 1 + 2 + 2 + 2 + data - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """update_datastore the diagnostic request on the given device.""" message: int | list | None = None # the clear operation does not return info if self.message == ModbusPlusOperation.CLEAR_STATISTICS: diff --git a/pymodbus/events.py b/pymodbus/pdu/events.py similarity index 90% rename from pymodbus/events.py rename to pymodbus/pdu/events.py index aa576a07f..19f58351b 100644 --- a/pymodbus/events.py +++ b/pymodbus/pdu/events.py @@ -87,21 +87,21 @@ class RemoteSendEvent(ModbusEvent): Bit Contents ----------------------------------------------------------- 0 Read Exception Sent (Exception Codes 1-3) - 1 Slave Abort Exception Sent (Exception Code 4) - 2 Slave Busy Exception Sent (Exception Codes 5-6) - 3 Slave Program NAK Exception Sent (Exception Code 7) + 1 Device Abort Exception Sent (Exception Code 4) + 2 Device Busy Exception Sent (Exception Codes 5-6) + 3 Device Program NAK Exception Sent (Exception Code 7) 4 Write Timeout Error Occurred 5 Currently in Listen Only Mode 6 1 7 0 """ - def __init__(self, read=False, slave_abort=False, slave_busy=False, slave_nak=False, write_timeout=False, listen=False): + def __init__(self, read=False, device_abort=False, device_busy=False, device_nak=False, write_timeout=False, listen=False): """Initialize a new event instance.""" self.read = read - self.slave_abort = slave_abort - self.slave_busy = slave_busy - self.slave_nak = slave_nak + self.device_abort = device_abort + self.device_busy = device_busy + self.device_nak = device_nak self.write_timeout = write_timeout self.listen = listen @@ -112,9 +112,9 @@ def encode(self): """ bits = [ self.read, - self.slave_abort, - self.slave_busy, - self.slave_nak, + self.device_abort, + self.device_busy, + self.device_nak, self.write_timeout, self.listen, ] @@ -130,9 +130,9 @@ def decode(self, event): # todo fix the start byte count # pylint: disable=fixme bits = unpack_bitstring(event) self.read = bits[0] - self.slave_abort = bits[1] - self.slave_busy = bits[2] - self.slave_nak = bits[3] + self.device_abort = bits[1] + self.device_busy = bits[2] + self.device_nak = bits[3] self.write_timeout = bits[4] self.listen = bits[5] diff --git a/pymodbus/pdu/file_message.py b/pymodbus/pdu/file_message.py index be939eaff..459e8baa8 100644 --- a/pymodbus/pdu/file_message.py +++ b/pymodbus/pdu/file_message.py @@ -4,7 +4,7 @@ import struct from dataclasses import dataclass -from pymodbus.datastore import ModbusSlaveContext +from pymodbus.datastore import ModbusDeviceContext from pymodbus.exceptions import ModbusException from pymodbus.pdu.pdu import ModbusPDU @@ -73,7 +73,7 @@ def get_response_pdu_size(self) -> int: """ return 1 + 7 * len(self.records) - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """Run a read exception status request against the store.""" for record in self.records: record.record_data = b'SERVER DUMMY RECORD.' @@ -169,7 +169,7 @@ def get_response_pdu_size(self) -> int: """ return 1 + 7 * len(self.records) - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """Run the write file record request against the context.""" return WriteFileRecordResponse(records=self.records, dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -237,7 +237,7 @@ def decode(self, data: bytes) -> None: """Decode the incoming request.""" self.address = struct.unpack(">H", data)[0] - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """Run a read exception status request against the store.""" values = [0, 1, 2, 3] # server dummy response (should be in datastore) return ReadFifoQueueResponse(values=values, dev_id=self.dev_id, transaction_id=self.transaction_id) diff --git a/pymodbus/pdu/mei_message.py b/pymodbus/pdu/mei_message.py index b3b81418f..6c8b6a49e 100644 --- a/pymodbus/pdu/mei_message.py +++ b/pymodbus/pdu/mei_message.py @@ -4,8 +4,8 @@ import struct from pymodbus.constants import DeviceInformation, MoreData -from pymodbus.datastore import ModbusSlaveContext -from pymodbus.device import DeviceInformationFactory, ModbusControlBlock +from pymodbus.datastore import ModbusDeviceContext +from pymodbus.pdu.device import DeviceInformationFactory, ModbusControlBlock from pymodbus.pdu.pdu import ExceptionResponse, ModbusPDU @@ -51,7 +51,7 @@ def decode(self, data: bytes) -> None: """Decode data part of the message.""" self.sub_function_code, self.read_code, self.object_id = struct.unpack(">BBB", data) - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """Run a read exception status request against the store.""" if not 0x00 <= self.object_id <= 0xFF: return ExceptionResponse(self.function_code, ExceptionResponse.ILLEGAL_VALUE) diff --git a/pymodbus/pdu/other_message.py b/pymodbus/pdu/other_message.py index fac49d41d..ae973c9f6 100644 --- a/pymodbus/pdu/other_message.py +++ b/pymodbus/pdu/other_message.py @@ -4,8 +4,8 @@ import struct from pymodbus.constants import ModbusStatus -from pymodbus.datastore import ModbusSlaveContext -from pymodbus.device import DeviceInformationFactory, ModbusControlBlock +from pymodbus.datastore import ModbusDeviceContext +from pymodbus.pdu.device import DeviceInformationFactory, ModbusControlBlock from pymodbus.pdu.pdu import ModbusPDU @@ -25,7 +25,7 @@ def encode(self) -> bytes: def decode(self, _data: bytes) -> None: """Decode data part of the message.""" - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """Run a read exception status request against the store.""" status = _MCB.Counter.summary() return ReadExceptionStatusResponse(status=status, dev_id=self.dev_id, transaction_id=self.transaction_id) @@ -98,7 +98,7 @@ def encode(self) -> bytes: def decode(self, _data: bytes) -> None: """Decode data part of the message.""" - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: """Run a read exception status request against the store.""" return GetCommEventLogResponse( status=True, @@ -146,8 +146,8 @@ def decode(self, data: bytes) -> None: self.events.append(int(data[i])) -class ReportSlaveIdRequest(ModbusPDU): - """ReportSlaveIdRequest.""" +class ReportDeviceIdRequest(ModbusPDU): + """ReportDeviceIdRequest.""" function_code = 0x11 rtu_frame_size = 4 @@ -159,8 +159,8 @@ def encode(self) -> bytes: def decode(self, _data: bytes) -> None: """Decode data part of the message.""" - async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: - """Run a report slave id request against the store.""" + async def update_datastore(self, _context: ModbusDeviceContext) -> ModbusPDU: + """Run a report device id request against the store.""" information = DeviceInformationFactory.get(_MCB) id_data = [] for v_item in information.values(): @@ -171,11 +171,13 @@ async def update_datastore(self, _context: ModbusSlaveContext) -> ModbusPDU: identifier = b"-".join(id_data) identifier = identifier or b"Pymodbus" - return ReportSlaveIdResponse(identifier=identifier, dev_id=self.dev_id, transaction_id=self.transaction_id) + return ReportDeviceIdResponse(identifier=identifier, dev_id=self.dev_id, transaction_id=self.transaction_id) +ID_ON = 0xFF +ID_OFF = 0x00 -class ReportSlaveIdResponse(ModbusPDU): - """ReportSlaveIdRequeste.""" +class ReportDeviceIdResponse(ModbusPDU): + """ReportDeviceIdRequeste.""" function_code = 0x11 rtu_byte_count_pos = 2 @@ -188,7 +190,7 @@ def __init__(self, identifier: bytes = b"\x00", status: bool = True, dev_id: int def encode(self) -> bytes: """Encode the response.""" - status = ModbusStatus.SLAVE_ON if self.status else ModbusStatus.SLAVE_OFF + status = ID_ON if self.status else ID_OFF length = len(self.identifier) + 1 packet = struct.pack(">B", length) packet += self.identifier # we assume it is already encoded @@ -204,4 +206,4 @@ def decode(self, data: bytes) -> None: self.byte_count = int(data[0]) self.identifier = data[1 : self.byte_count + 1] status = int(data[-1]) - self.status = status == ModbusStatus.SLAVE_ON + self.status = status == ID_ON diff --git a/pymodbus/pdu/pdu.py b/pymodbus/pdu/pdu.py index 3b6508172..431059f8c 100644 --- a/pymodbus/pdu/pdu.py +++ b/pymodbus/pdu/pdu.py @@ -5,8 +5,8 @@ import struct from abc import abstractmethod -from pymodbus.datastore import ModbusSlaveContext -from pymodbus.exceptions import NotImplementedException +from pymodbus.datastore import ModbusDeviceContext +from pymodbus.exceptions import ModbusIOException, NotImplementedException from pymodbus.logging import Log @@ -29,6 +29,8 @@ def __init__(self, ) -> None: """Initialize the base data for a modbus request.""" self.dev_id: int = dev_id + if dev_id > 255: + raise ModbusIOException(f"Invalid ID {dev_id}") self.transaction_id: int = transaction_id self.address: int = address self.bits: list[bool] = bits or [] @@ -80,7 +82,7 @@ def encode(self) -> bytes: def decode(self, data: bytes) -> None: """Decode data part of the message.""" - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Run request against a datastore.""" _ = context return ExceptionResponse(0, 0) @@ -107,9 +109,9 @@ class ExceptionResponse(ModbusPDU): ILLEGAL_FUNCTION = 0x01 ILLEGAL_ADDRESS = 0x02 ILLEGAL_VALUE = 0x03 - SLAVE_FAILURE = 0x04 + DEVICE_FAILURE = 0x04 ACKNOWLEDGE = 0x05 - SLAVE_BUSY = 0x06 + DEVICE_BUSY = 0x06 NEGATIVE_ACKNOWLEDGE = 0x07 MEMORY_PARITY_ERROR = 0x08 GATEWAY_PATH_UNAVIABLE = 0x0A @@ -119,10 +121,10 @@ def __init__( self, function_code: int, exception_code: int = 0, - slave: int = 1, + device_id: int = 1, transaction: int = 0) -> None: """Initialize the modbus exception response.""" - super().__init__(transaction_id=transaction, dev_id=slave) + super().__init__(transaction_id=transaction, dev_id=device_id) self.function_code = function_code | 0x80 self.exception_code = exception_code Log.error(f"Exception response {self.function_code} / {self.exception_code}") diff --git a/pymodbus/pdu/register_message.py b/pymodbus/pdu/register_message.py index 656e3c571..67b7105bf 100644 --- a/pymodbus/pdu/register_message.py +++ b/pymodbus/pdu/register_message.py @@ -4,7 +4,7 @@ import struct from typing import cast -from pymodbus.datastore import ModbusSlaveContext +from pymodbus.datastore import ModbusDeviceContext from pymodbus.exceptions import ModbusIOException from pymodbus.pdu.pdu import ExceptionResponse, ModbusPDU @@ -32,7 +32,7 @@ def get_response_pdu_size(self) -> int: """ return 1 + 1 + 2 * self.count - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Run a read holding request against a datastore.""" values = cast(list[int], await context.async_getValues( self.function_code, self.address, self.count @@ -131,7 +131,7 @@ def decode(self, data: bytes) -> None: register = struct.unpack(">H", data[i : i + 2])[0] self.write_registers.append(register) - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Run a write single register request against a datastore.""" if not (1 <= self.read_count <= 0x07D): return ExceptionResponse(self.function_code, ExceptionResponse.ILLEGAL_VALUE) @@ -178,7 +178,7 @@ def decode(self, data: bytes) -> None: class WriteSingleRegisterRequest(WriteSingleRegisterResponse): """WriteSingleRegisterRequest.""" - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Run a write single register request against a datastore.""" if not 0 <= self.registers[0] <= 0xFFFF: return ExceptionResponse(self.function_code, ExceptionResponse.ILLEGAL_VALUE) @@ -217,7 +217,7 @@ def decode(self, data: bytes) -> None: for idx in range(5, (self.count * 2) + 5, 2): self.registers.append(struct.unpack(">H", data[idx : idx + 2])[0]) - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Run a write single register request against a datastore.""" if not 1 <= self.count <= 0x07B: return ExceptionResponse(self.function_code, ExceptionResponse.ILLEGAL_VALUE) @@ -269,7 +269,7 @@ def decode(self, data: bytes) -> None: """Decode the incoming request.""" self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data) - async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU: + async def update_datastore(self, context: ModbusDeviceContext) -> ModbusPDU: """Run a mask write register request against the store.""" if not 0x0000 <= self.and_mask <= 0xFFFF: return ExceptionResponse(self.function_code, ExceptionResponse.ILLEGAL_VALUE) diff --git a/pymodbus/server/base.py b/pymodbus/server/base.py index ae46c5068..e7d73ec1c 100644 --- a/pymodbus/server/base.py +++ b/pymodbus/server/base.py @@ -6,10 +6,10 @@ from contextlib import suppress from pymodbus.datastore import ModbusServerContext -from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType from pymodbus.logging import Log from pymodbus.pdu import DecodePDU, ModbusPDU +from pymodbus.pdu.device import ModbusControlBlock, ModbusDeviceIdentification from pymodbus.transport import CommParams, ModbusProtocol from .requesthandler import ServerRequestHandler @@ -24,7 +24,7 @@ def __init__( # pylint: disable=too-many-arguments self, params: CommParams, context: ModbusServerContext | None, - ignore_missing_slaves: bool, + ignore_missing_devices: bool, broadcast_enable: bool, identity: ModbusDeviceIdentification | None, framer: FramerType, @@ -45,7 +45,7 @@ def __init__( # pylint: disable=too-many-arguments self.decoder.register(func) self.context = context or ModbusServerContext() self.control = ModbusControlBlock() - self.ignore_missing_slaves = ignore_missing_slaves + self.ignore_missing_devices = ignore_missing_devices self.broadcast_enable = broadcast_enable self.trace_packet = trace_packet self.trace_pdu = trace_pdu diff --git a/pymodbus/server/requesthandler.py b/pymodbus/server/requesthandler.py index 0ddf90931..dff111ede 100644 --- a/pymodbus/server/requesthandler.py +++ b/pymodbus/server/requesthandler.py @@ -4,7 +4,7 @@ import asyncio import traceback -from pymodbus.exceptions import ModbusIOException, NoSuchSlaveException +from pymodbus.exceptions import ModbusIOException, NoSuchIdException from pymodbus.logging import Log from pymodbus.pdu.pdu import ExceptionResponse from pymodbus.transaction import TransactionManager @@ -43,14 +43,6 @@ def callback_new_connection(self) -> ModbusProtocol: """Call when listener receive new connection request.""" raise RuntimeError("callback_new_connection should never be called") - def callback_connected(self) -> None: - """Call when connection is succcesfull.""" - super().callback_connected() - slaves = self.server.context.slaves() - if self.server.broadcast_enable: - if 0 not in slaves: - slaves.append(0) - def callback_disconnected(self, call_exc: Exception | None) -> None: """Call when connection is lost.""" super().callback_disconnected(call_exc) @@ -100,17 +92,17 @@ async def handle_request(self): try: if self.server.broadcast_enable and not self.last_pdu.dev_id: broadcast = True - # if broadcasting then execute on all slave contexts, + # if broadcasting then execute on all device contexts, # note response will be ignored - for dev_id in self.server.context.slaves(): + for dev_id in self.server.context.device_id(): response = await self.last_pdu.update_datastore(self.server.context[dev_id]) else: context = self.server.context[self.last_pdu.dev_id] response = await self.last_pdu.update_datastore(context) - except NoSuchSlaveException: - Log.error("requested slave does not exist: {}", self.last_pdu.dev_id) - if self.server.ignore_missing_slaves: + except NoSuchIdException: + Log.error("requested device id does not exist: {}", self.last_pdu.dev_id) + if self.server.ignore_missing_devices: return # the client will simply timeout waiting for a response response = ExceptionResponse(0x00, ExceptionResponse.GATEWAY_NO_RESPONSE) except Exception as exc: # pylint: disable=broad-except @@ -119,7 +111,7 @@ async def handle_request(self): exc, traceback.format_exc(), ) - response = ExceptionResponse(0x00, ExceptionResponse.SLAVE_FAILURE) + response = ExceptionResponse(0x00, ExceptionResponse.DEVICE_FAILURE) # no response when broadcasting if not broadcast: response.transaction_id = self.last_pdu.transaction_id diff --git a/pymodbus/server/server.py b/pymodbus/server/server.py index 4c5549b13..8fbd16738 100644 --- a/pymodbus/server/server.py +++ b/pymodbus/server/server.py @@ -4,9 +4,9 @@ from collections.abc import Callable from pymodbus.datastore import ModbusServerContext -from pymodbus.device import ModbusDeviceIdentification from pymodbus.framer import FramerType from pymodbus.pdu import ModbusPDU +from pymodbus.pdu.device import ModbusDeviceIdentification from pymodbus.transport import CommParams, CommType from .base import ModbusBaseServer @@ -26,7 +26,7 @@ def __init__( # pylint: disable=too-many-arguments framer=FramerType.SOCKET, identity: ModbusDeviceIdentification | None = None, address: tuple[str, int] = ("", 502), - ignore_missing_slaves: bool = False, + ignore_missing_devices: bool = False, broadcast_enable: bool = False, trace_packet: Callable[[bool, bytes], bytes] | None = None, trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, @@ -42,8 +42,7 @@ def __init__( # pylint: disable=too-many-arguments :param framer: The framer strategy to use :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. - :param ignore_missing_slaves: True to not send errors on a request - to a missing slave + :param ignore_missing_devices: True to not send errors on a missing device :param broadcast_enable: True to treat dev_id 0 as broadcast address, False to treat 0 as any other dev_id :param trace_packet: Called with bytestream received/to be sent @@ -66,7 +65,7 @@ def __init__( # pylint: disable=too-many-arguments super().__init__( params, context, - ignore_missing_slaves, + ignore_missing_devices, broadcast_enable, identity, framer, @@ -95,7 +94,7 @@ def __init__( # pylint: disable=too-many-arguments certfile=None, keyfile=None, password=None, - ignore_missing_slaves=False, + ignore_missing_devices=False, broadcast_enable=False, trace_packet: Callable[[bool, bytes], bytes] | None = None, trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, @@ -116,8 +115,7 @@ def __init__( # pylint: disable=too-many-arguments :param certfile: The cert file path for TLS (used if sslctx is None) :param keyfile: The key file path for TLS (used if sslctx is None) :param password: The password for for decrypting the private key file - :param ignore_missing_slaves: True to not send errors on a request - to a missing slave + :param ignore_missing_devices: True to not send errors on a missing device :param broadcast_enable: True to treat dev_id 0 as broadcast address, False to treat 0 as any other dev_id :param trace_packet: Called with bytestream received/to be sent @@ -140,7 +138,7 @@ def __init__( # pylint: disable=too-many-arguments framer=framer, identity=identity, address=address, - ignore_missing_slaves=ignore_missing_slaves, + ignore_missing_devices=ignore_missing_devices, broadcast_enable=broadcast_enable, trace_packet=trace_packet, trace_pdu=trace_pdu, @@ -163,7 +161,7 @@ def __init__( # pylint: disable=too-many-arguments framer=FramerType.SOCKET, identity: ModbusDeviceIdentification | None = None, address: tuple[str, int] = ("", 502), - ignore_missing_slaves: bool = False, + ignore_missing_devices: bool = False, broadcast_enable: bool = False, trace_packet: Callable[[bool, bytes], bytes] | None = None, trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, @@ -179,8 +177,7 @@ def __init__( # pylint: disable=too-many-arguments :param framer: The framer strategy to use :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. - :param ignore_missing_slaves: True to not send errors on a request - to a missing slave + :param ignore_missing_devices: True to not send errors on a missing device :param broadcast_enable: True to treat dev_id 0 as broadcast address, False to treat 0 as any other dev_id :param trace_packet: Called with bytestream received/to be sent @@ -200,7 +197,7 @@ def __init__( # pylint: disable=too-many-arguments super().__init__( params, context, - ignore_missing_slaves, + ignore_missing_devices, broadcast_enable, identity, framer, @@ -223,7 +220,7 @@ def __init__( context: ModbusServerContext, *, framer: FramerType = FramerType.RTU, - ignore_missing_slaves: bool = False, + ignore_missing_devices: bool = False, identity: ModbusDeviceIdentification | None = None, broadcast_enable: bool = False, trace_packet: Callable[[bool, bytes], bytes] | None = None, @@ -246,8 +243,7 @@ def __init__( :param baudrate: The baud rate to use for the serial device :param timeout: The timeout to use for the serial device :param handle_local_echo: (optional) Discard local echo from dongle. - :param ignore_missing_slaves: True to not send errors on a request - to a missing slave + :param ignore_missing_devices: True to not send errors on a missing device :param broadcast_enable: True to treat dev_id 0 as broadcast address, False to treat 0 as any other dev_id :param reconnect_delay: reconnect delay in seconds @@ -272,7 +268,7 @@ def __init__( super().__init__( params, context, - ignore_missing_slaves, + ignore_missing_devices, broadcast_enable, identity, framer, diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 424234264..a15ca84ec 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -22,9 +22,9 @@ from pymodbus.datastore import ModbusServerContext, ModbusSimulatorContext from pymodbus.datastore.simulator import Label -from pymodbus.device import ModbusDeviceIdentification from pymodbus.logging import Log from pymodbus.pdu import DecodePDU +from pymodbus.pdu.device import ModbusDeviceIdentification from pymodbus.server.server import ( ModbusSerialServer, ModbusTcpServer, @@ -155,11 +155,11 @@ def __init__( datastore = None if "device_id" in server: # Designated ModBus unit address. Will only serve data if the address matches - datastore = ModbusServerContext(slaves={int(server["device_id"]): self.datastore_context}, single=False) + datastore = ModbusServerContext(devices={int(server["device_id"]): self.datastore_context}, single=False) del server["device_id"] else: # Will server any request regardless of addressing - datastore = ModbusServerContext(slaves=self.datastore_context, single=True) + datastore = ModbusServerContext(devices=self.datastore_context, single=True) comm = comm_class[server.pop("comm")] framer = server.pop("framer") @@ -367,9 +367,9 @@ def build_html_calls(self, params: dict, html: str) -> str: (1, "ILLEGAL_FUNCTION"), (2, "ILLEGAL_ADDRESS"), (3, "ILLEGAL_VALUE"), - (4, "SLAVE_FAILURE"), + (4, "DEVICE_FAILURE"), (5, "ACKNOWLEDGE"), - (6, "SLAVE_BUSY"), + (6, "DEVICE_BUSY"), (7, "MEMORY_PARITY_ERROR"), (10, "GATEWAY_PATH_UNAVIABLE"), (11, "GATEWAY_NO_RESPONSE"), @@ -529,9 +529,9 @@ def build_json_calls(self, params: dict) -> dict: (1, "ILLEGAL_FUNCTION"), (2, "ILLEGAL_ADDRESS"), (3, "ILLEGAL_VALUE"), - (4, "SLAVE_FAILURE"), + (4, "DEVICE_FAILURE"), (5, "ACKNOWLEDGE"), - (6, "SLAVE_BUSY"), + (6, "DEVICE_BUSY"), (7, "MEMORY_PARITY_ERROR"), (10, "GATEWAY_PATH_UNAVIABLE"), (11, "GATEWAY_NO_RESPONSE"), diff --git a/pymodbus/server/simulator/setup.json b/pymodbus/server/simulator/setup.json index 94c63f4fb..ef41ff6f7 100644 --- a/pymodbus/server/simulator/setup.json +++ b/pymodbus/server/simulator/setup.json @@ -4,7 +4,7 @@ "comm": "tcp", "host": "0.0.0.0", "port": 5020, - "ignore_missing_slaves": false, + "ignore_missing_devices": false, "framer": "socket", "identity": { "VendorName": "pymodbus", @@ -40,7 +40,7 @@ "port": 5020, "certfile": "certificates/pymodbus.crt", "keyfile": "certificates/pymodbus.key", - "ignore_missing_slaves": false, + "ignore_missing_devices": false, "framer": "tls", "identity": { "VendorName": "pymodbus", @@ -55,7 +55,7 @@ "comm": "udp", "host": "0.0.0.0", "port": 5020, - "ignore_missing_slaves": false, + "ignore_missing_devices": false, "framer": "socket", "identity": { "VendorName": "pymodbus", diff --git a/test/client/test_client.py b/test/client/test_client.py index 2ddfbeb28..603b41227 100755 --- a/test/client/test_client.py +++ b/test/client/test_client.py @@ -60,11 +60,11 @@ class TestMixin: ("diag_read_bus_message_count", 0, pdu_diag.ReturnBusMessageCountRequest), ("diag_read_bus_comm_error_count",0, pdu_diag.ReturnBusCommunicationErrorCountRequest), ("diag_read_bus_exception_error_count", 0, pdu_diag.ReturnBusExceptionErrorCountRequest), - ("diag_read_slave_message_count", 0, pdu_diag.ReturnSlaveMessageCountRequest), - ("diag_read_slave_no_response_count", 0, pdu_diag.ReturnSlaveNoResponseCountRequest), - ("diag_read_slave_nak_count", 0, pdu_diag.ReturnSlaveNAKCountRequest), - ("diag_read_slave_busy_count", 0, pdu_diag.ReturnSlaveBusyCountRequest), - ("diag_read_bus_char_overrun_count", 0, pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest), + ("diag_read_device_message_count", 0, pdu_diag.ReturnDeviceMessageCountRequest), + ("diag_read_device_no_response_count", 0, pdu_diag.ReturnDeviceNoResponseCountRequest), + ("diag_read_device_nak_count", 0, pdu_diag.ReturnDeviceNAKCountRequest), + ("diag_read_device_busy_count", 0, pdu_diag.ReturnDeviceBusyCountRequest), + ("diag_read_bus_char_overrun_count", 0, pdu_diag.ReturnDeviceBusCharacterOverrunCountRequest), ("diag_read_iop_overrun_count", 0, pdu_diag.ReturnIopOverrunCountRequest), ("diag_clear_overrun_counter", 0, pdu_diag.ClearOverrunCountRequest), ("diag_getclear_modbus_response", 0, pdu_diag.GetClearModbusPlusRequest), @@ -75,7 +75,7 @@ class TestMixin: ("readwrite_registers", 0, pdu_reg.ReadWriteMultipleRegistersRequest), ("readwrite_registers", 6, pdu_reg.ReadWriteMultipleRegistersRequest), ("mask_write_register", 1, pdu_reg.MaskWriteRegisterRequest), - ("report_slave_id", 0, pdu_other_msg.ReportSlaveIdRequest), + ("report_device_id", 0, pdu_other_msg.ReportDeviceIdRequest), ("read_file_record", 7, pdu_file_msg.ReadFileRecordRequest), ("write_file_record", 7, pdu_file_msg.WriteFileRecordRequest), ("read_fifo_queue", 1, pdu_file_msg.ReadFifoQueueRequest), diff --git a/test/client/test_client_faulty_response.py b/test/client/test_client_faulty_response.py index 2974f7773..dec879baf 100644 --- a/test/client/test_client_faulty_response.py +++ b/test/client/test_client_faulty_response.py @@ -1,4 +1,4 @@ -"""Test server working as slave on a multidrop RS485 line.""" +"""Test server working as a device on a multidrop RS485 line.""" import pytest diff --git a/test/client/test_client_sync.py b/test/client/test_client_sync.py index 07e8c8cf3..0f88d29af 100755 --- a/test/client/test_client_sync.py +++ b/test/client/test_client_sync.py @@ -80,9 +80,9 @@ def test_udp_client_recv_duplicate(self): client.socket = mockSocket(copy_send=False) client.socket.mock_prepare_receive(test_msg) client.socket.mock_prepare_receive(test_msg) - reply_ok = client.read_input_registers(0x820, count=1, slave=1) + reply_ok = client.read_input_registers(0x820, count=1, device_id=1) assert not reply_ok.isError() - reply_ok = client.read_input_registers(0x40, count=10, slave=1) + reply_ok = client.read_input_registers(0x40, count=10, device_id=1) assert not reply_ok.isError() client.close() @@ -396,7 +396,7 @@ def test_serial_client_recv_split(self): client.socket.mock_prepare_receive(b'\x11\x03\x06\xAE') client.socket.mock_prepare_receive(b'\x41\x56\x52\x43\x40\x49') client.socket.mock_prepare_receive(b'\xAD') - reply_ok = client.read_input_registers(0x820, count=3, slave=17) + reply_ok = client.read_input_registers(0x820, count=3, device_id=17) assert not reply_ok.isError() client.close() diff --git a/test/conftest.py b/test/conftest.py index c8a7a2322..3dfdab5a1 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -8,7 +8,7 @@ import pytest import pytest_asyncio -from pymodbus.datastore import ModbusBaseSlaveContext +from pymodbus.datastore import ModbusBaseDeviceContext from pymodbus.server import ServerAsyncStop from pymodbus.transport import NULLMODEM_HOST, CommParams, CommType from pymodbus.transport.transport import NullModem @@ -208,7 +208,7 @@ async def _check_system_health(): @pytest.fixture(name="mock_context") def define_mock_context(): """Define context class.""" - class MockContext(ModbusBaseSlaveContext): + class MockContext(ModbusBaseDeviceContext): """Mock context.""" def __init__(self, valid=False, default=True): @@ -226,7 +226,7 @@ def setValues(self, _fc, _address, _values): return MockContext -class MockLastValuesContext(ModbusBaseSlaveContext): +class MockLastValuesContext(ModbusBaseDeviceContext): """Mock context.""" def __init__(self, valid=False, default=True): diff --git a/test/examples/test_helper.py b/test/examples/test_helper.py index 8ce22239d..7573d87e6 100644 --- a/test/examples/test_helper.py +++ b/test/examples/test_helper.py @@ -18,7 +18,7 @@ def test_commandline_server_defaults(self): assert args.framer assert args.port assert args.store - assert not args.slaves + assert not args.device_ids assert not args.context def test_commandline_client_defaults(self): diff --git a/test/framer/test_multidrop.py b/test/framer/test_multidrop.py index 736572935..0bf11a995 100644 --- a/test/framer/test_multidrop.py +++ b/test/framer/test_multidrop.py @@ -1,4 +1,4 @@ -"""Test server working as slave on a multidrop RS485 line.""" +"""Test server working as device on a multidrop RS485 line.""" import pytest diff --git a/test/not_updated/test_device.py b/test/not_updated/test_device.py index 66c1fd09b..eea528374 100644 --- a/test/not_updated/test_device.py +++ b/test/not_updated/test_device.py @@ -1,12 +1,12 @@ """Test device.""" from pymodbus.constants import DeviceInformation -from pymodbus.device import ( +from pymodbus.pdu.device import ( DeviceInformationFactory, ModbusControlBlock, ModbusDeviceIdentification, ModbusPlusStatistics, ) -from pymodbus.events import RemoteReceiveEvent +from pymodbus.pdu.events import RemoteReceiveEvent # ---------------------------------------------------------------------------# @@ -202,21 +202,21 @@ def test_modbus_control_block_counters(self): assert not self.control.Counter.BusMessage for _ in range(10): self.control.Counter.BusMessage += 1 - self.control.Counter.SlaveMessage += 1 + self.control.Counter.DeviceMessage += 1 assert self.control.Counter.BusMessage == 10 self.control.Counter.BusMessage = 0x00 assert not self.control.Counter.BusMessage - assert self.control.Counter.SlaveMessage == 10 + assert self.control.Counter.DeviceMessage == 10 self.control.Counter.reset() - assert not self.control.Counter.SlaveMessage + assert not self.control.Counter.DeviceMessage def test_modbus_control_block_update(self): """Tests the MCB counters update methods.""" - values = {"SlaveMessage": 5, "BusMessage": 5} + values = {"DeviceMessage": 5, "BusMessage": 5} self.control.Counter.BusMessage += 1 - self.control.Counter.SlaveMessage += 1 + self.control.Counter.DeviceMessage += 1 self.control.Counter.update(values) - assert self.control.Counter.SlaveMessage == 6 + assert self.control.Counter.DeviceMessage == 6 assert self.control.Counter.BusMessage == 6 def test_modbus_control_block_iterator(self): @@ -236,8 +236,8 @@ def test_modbus_control_block_counter_summary(self): assert not self.control.Counter.summary() for _ in range(10): self.control.Counter.BusMessage += 1 - self.control.Counter.SlaveMessage += 1 - self.control.Counter.SlaveNAK += 1 + self.control.Counter.DeviceMessage += 1 + self.control.Counter.DeviceNAK += 1 self.control.Counter.BusCharacterOverrun += 1 assert self.control.Counter.summary() == 0xA9 self.control.Counter.reset() diff --git a/test/not_updated/test_events.py b/test/not_updated/test_events.py index 3cec6672d..f52b920b8 100644 --- a/test/not_updated/test_events.py +++ b/test/not_updated/test_events.py @@ -1,13 +1,13 @@ """Test events.""" import pytest -from pymodbus.events import ( +from pymodbus.exceptions import ParameterException +from pymodbus.pdu.events import ( CommunicationRestartEvent, EnteredListenModeEvent, RemoteReceiveEvent, RemoteSendEvent, ) -from pymodbus.exceptions import ParameterException class TestEvents: @@ -28,9 +28,9 @@ def test_remote_sent_event(self): assert result == b"\x40" event.decode(b"\x7f") assert event.read - assert event.slave_abort - assert event.slave_busy - assert event.slave_nak + assert event.device_abort + assert event.device_busy + assert event.device_nak assert event.write_timeout assert event.listen @@ -38,9 +38,9 @@ def test_remote_sent_event_encode(self): """Test remote sent event encode.""" arguments = { "read": True, - "slave_abort": True, - "slave_busy": True, - "slave_nak": True, + "device_abort": True, + "device_busy": True, + "device_nak": True, "write_timeout": True, "listen": True, } diff --git a/test/not_updated/test_remote_datastore.py b/test/not_updated/test_remote_datastore.py index bf27144d6..b81265de9 100644 --- a/test/not_updated/test_remote_datastore.py +++ b/test/not_updated/test_remote_datastore.py @@ -4,7 +4,7 @@ import pytest -from pymodbus.datastore.remote import RemoteSlaveContext +from pymodbus.datastore.remote import RemoteDeviceContext from pymodbus.exceptions import NotImplementedException from pymodbus.pdu import ExceptionResponse from pymodbus.pdu.bit_message import ReadCoilsResponse, WriteMultipleCoilsResponse @@ -14,40 +14,40 @@ class TestRemoteDataStore: """Unittest for the pymodbus.datastore.remote module.""" - def test_remote_slave_context(self): - """Test a modbus remote slave context.""" - context = RemoteSlaveContext(None) + def test_remote_device_context(self): + """Test a modbus remote device context.""" + context = RemoteDeviceContext(None) assert str(context) with pytest.raises(NotImplementedException): context.reset() - def test_remote_slave_set_values(self): - """Test setting values against a remote slave context.""" + def test_remote_device_set_values(self): + """Test setting values against a remote device context.""" client = mock.MagicMock() client.write_coils = lambda a, b: WriteMultipleCoilsResponse() client.write_registers = lambda a, b: ExceptionResponse(0x10, 0x02) - context = RemoteSlaveContext(client) + context = RemoteDeviceContext(client) context.setValues(0x0F, 0, [1]) # result = context.setValues(0x10, 1, [1]) context.setValues(0x10, 1, [1]) # assert result.exception_code == 0x02 # assert result.function_code == 0x90 - async def test_remote_slave_async_set_values(self): - """Test setting values against a remote slave context.""" + async def test_remote_device_async_set_values(self): + """Test setting values against a remote device context.""" client = mock.MagicMock() client.write_coils = mock.MagicMock(return_value=WriteMultipleCoilsResponse()) client.write_registers = mock.MagicMock( return_value=ExceptionResponse(0x10, 0x02) ) - context = RemoteSlaveContext(client) + context = RemoteDeviceContext(client) await context.async_setValues(0x0F, 0, [1]) await context.async_setValues(0x10, 1, [1]) - def test_remote_slave_get_values(self): - """Test getting values from a remote slave context.""" + def test_remote_device_get_values(self): + """Test getting values from a remote device context.""" client = mock.MagicMock() pdu = ReadCoilsResponse(bits=[True] * 10) read_input_reg_res = ReadInputRegistersResponse(registers=[10] * 10) @@ -56,7 +56,7 @@ def test_remote_slave_get_values(self): client.read_input_registers = lambda a, count=1: read_input_reg_res client.read_holding_registers = lambda a, count=1: exception_response - context = RemoteSlaveContext(client) + context = RemoteDeviceContext(client) result = context.getValues(1, 0, 10) assert result == [1] * 10 @@ -66,8 +66,8 @@ def test_remote_slave_get_values(self): result = context.getValues(3, 0, 10) assert result != [10] * 10 - async def test_remote_slave_async_get_values(self): - """Test getting values from a remote slave context.""" + async def test_remote_device_async_get_values(self): + """Test getting values from a remote device context.""" client = mock.MagicMock() pdu = ReadCoilsResponse(bits=[True] * 10) client.read_coils = mock.MagicMock(return_value=pdu) @@ -78,7 +78,7 @@ async def test_remote_slave_async_get_values(self): return_value=ExceptionResponse(0x15) ) - context = RemoteSlaveContext(client) + context = RemoteDeviceContext(client) result = await context.async_getValues(1, 0, 10) assert result == [1] * 10 diff --git a/test/pdu/test_decoders.py b/test/pdu/test_decoders.py index b7ab899b3..3bf1664ac 100644 --- a/test/pdu/test_decoders.py +++ b/test/pdu/test_decoders.py @@ -24,7 +24,7 @@ class TestModbusPDU: (0x0C, b"\x0c"), # get comm event log (0x0F, b"\x0f\x00\x01\x00\x08\x01\x00\xff"), # write multiple coils (0x10, b"\x10\x00\x01\x00\x02\x04\0xff\xff"), # write multiple registers - (0x11, b"\x11"), # report slave id + (0x11, b"\x11"), # report device id ( 0x14, b"\x14\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02", @@ -55,7 +55,7 @@ class TestModbusPDU: (0x0C, b"\x0c\x08\x00\x00\x01\x08\x01\x21\x20\x00"), # get comm event log (0x0F, b"\x0f\x00\x01\x00\x08"), # write multiple coils (0x10, b"\x10\x00\x01\x00\x02"), # write multiple registers - (0x11, b"\x11\x03\x05\x01\x54"), # report slave id (device specific) + (0x11, b"\x11\x03\x05\x01\x54"), # report device id (device specific) ( 0x14, b"\x14\x0c\x05\x06\x0d\xfe\x00\x20\x05\x06\x33\xcd\x00\x40", @@ -79,7 +79,7 @@ class TestModbusPDU: (0x83, b"\x83\x03\x50\xf1"), # illegal data value exception (0x84, b"\x84\x04\x13\x03"), # skave device failure exception (0x85, b"\x85\x05\xd3\x53"), # acknowledge exception - (0x86, b"\x86\x06\x93\xa2"), # slave device busy exception + (0x86, b"\x86\x06\x93\xa2"), # device busy exception (0x87, b"\x87\x08\x53\xf2"), # memory parity exception (0x88, b"\x88\x0a\x16\x06"), # gateway path unavailable exception (0x89, b"\x89\x0b\xd6\x56"), # gateway target failed exception diff --git a/test/pdu/test_diag_messages.py b/test/pdu/test_diag_messages.py index fed1bac1e..edfbea890 100644 --- a/test/pdu/test_diag_messages.py +++ b/test/pdu/test_diag_messages.py @@ -22,22 +22,22 @@ ReturnBusExceptionErrorCountResponse, ReturnBusMessageCountRequest, ReturnBusMessageCountResponse, + ReturnDeviceBusCharacterOverrunCountRequest, + ReturnDeviceBusCharacterOverrunCountResponse, + ReturnDeviceBusyCountRequest, + ReturnDeviceBusyCountResponse, + ReturnDeviceMessageCountRequest, + ReturnDeviceMessageCountResponse, + ReturnDeviceNAKCountRequest, + ReturnDeviceNAKCountResponse, + ReturnDeviceNoResponseCountRequest, + ReturnDeviceNoResponseCountResponse, ReturnDiagnosticRegisterRequest, ReturnDiagnosticRegisterResponse, ReturnIopOverrunCountRequest, ReturnIopOverrunCountResponse, ReturnQueryDataRequest, ReturnQueryDataResponse, - ReturnSlaveBusCharacterOverrunCountRequest, - ReturnSlaveBusCharacterOverrunCountResponse, - ReturnSlaveBusyCountRequest, - ReturnSlaveBusyCountResponse, - ReturnSlaveMessageCountRequest, - ReturnSlaveMessageCountResponse, - ReturnSlaveNAKCountRequest, - ReturnSlaveNAKCountResponse, - ReturnSlaveNoResponseCountRequest, - ReturnSlaveNoResponseCountResponse, ) @@ -70,16 +70,16 @@ class TestDataStore: b"\x00\x0d\x00\x00", b"\x00\x0d\x00\x00", ), - (ReturnSlaveMessageCountRequest, b"\x00\x0e\x00\x00", b"\x00\x0e\x00\x00"), + (ReturnDeviceMessageCountRequest, b"\x00\x0e\x00\x00", b"\x00\x0e\x00\x00"), ( - ReturnSlaveNoResponseCountRequest, + ReturnDeviceNoResponseCountRequest, b"\x00\x0f\x00\x00", b"\x00\x0f\x00\x00", ), - (ReturnSlaveNAKCountRequest, b"\x00\x10\x00\x00", b"\x00\x10\x00\x00"), - (ReturnSlaveBusyCountRequest, b"\x00\x11\x00\x00", b"\x00\x11\x00\x00"), + (ReturnDeviceNAKCountRequest, b"\x00\x10\x00\x00", b"\x00\x10\x00\x00"), + (ReturnDeviceBusyCountRequest, b"\x00\x11\x00\x00", b"\x00\x11\x00\x00"), ( - ReturnSlaveBusCharacterOverrunCountRequest, + ReturnDeviceBusCharacterOverrunCountRequest, b"\x00\x12\x00\x00", b"\x00\x12\x00\x00", ), @@ -105,11 +105,11 @@ class TestDataStore: (ReturnBusMessageCountResponse, b"\x00\x0b\x00\x00"), (ReturnBusCommunicationErrorCountResponse, b"\x00\x0c\x00\x00"), (ReturnBusExceptionErrorCountResponse, b"\x00\x0d\x00\x00"), - (ReturnSlaveMessageCountResponse, b"\x00\x0e\x00\x00"), - (ReturnSlaveNoResponseCountResponse, b"\x00\x0f\x00\x00"), - (ReturnSlaveNAKCountResponse, b"\x00\x10\x00\x00"), - (ReturnSlaveBusyCountResponse, b"\x00\x11\x00\x00"), - (ReturnSlaveBusCharacterOverrunCountResponse, b"\x00\x12\x00\x00"), + (ReturnDeviceMessageCountResponse, b"\x00\x0e\x00\x00"), + (ReturnDeviceNoResponseCountResponse, b"\x00\x0f\x00\x00"), + (ReturnDeviceNAKCountResponse, b"\x00\x10\x00\x00"), + (ReturnDeviceBusyCountResponse, b"\x00\x11\x00\x00"), + (ReturnDeviceBusCharacterOverrunCountResponse, b"\x00\x12\x00\x00"), (ReturnIopOverrunCountResponse, b"\x00\x13\x00\x00"), (ClearOverrunCountResponse, b"\x00\x14\x00\x00"), (GetClearModbusPlusResponse, b"\x00\x15\x00\x04" + b"\x00\x00" * 55), diff --git a/test/pdu/test_mei_messages.py b/test/pdu/test_mei_messages.py index 40153f3a3..a16b1e5f7 100644 --- a/test/pdu/test_mei_messages.py +++ b/test/pdu/test_mei_messages.py @@ -6,7 +6,7 @@ import pytest from pymodbus.constants import DeviceInformation -from pymodbus.device import ModbusControlBlock +from pymodbus.pdu.device import ModbusControlBlock from pymodbus.pdu.mei_message import ( ReadDeviceInformationRequest, ReadDeviceInformationResponse, diff --git a/test/pdu/test_other_messages.py b/test/pdu/test_other_messages.py index 725a648a0..e2bf7dbbd 100644 --- a/test/pdu/test_other_messages.py +++ b/test/pdu/test_other_messages.py @@ -11,14 +11,14 @@ class TestOtherMessage: pymodbus_message.ReadExceptionStatusRequest, pymodbus_message.GetCommEventCounterRequest, pymodbus_message.GetCommEventLogRequest, - pymodbus_message.ReportSlaveIdRequest, + pymodbus_message.ReportDeviceIdRequest, ] responses = [ pymodbus_message.ReadExceptionStatusResponse(0x12), pymodbus_message.GetCommEventCounterResponse(0x12), pymodbus_message.GetCommEventLogResponse, - pymodbus_message.ReportSlaveIdResponse(0x12), + pymodbus_message.ReportDeviceIdResponse(0x12), ] def test_other_messages_to_string(self): @@ -85,8 +85,8 @@ def test_get_comm_event_log_with_events(self): assert response.event_count == 0x12 assert response.events == [0x12, 0x34, 0x56] - async def test_report_slave_id_request(self): - """Test report slave id request.""" + async def test_report_device_id_request(self): + """Test report device_id request.""" with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: # First test regular identity strings identity = { @@ -103,7 +103,7 @@ async def test_report_slave_id_request(self): dif.get.return_value = identity expected_identity = "-".join(identity.values()).encode() - request = pymodbus_message.ReportSlaveIdRequest() + request = pymodbus_message.ReportDeviceIdRequest() response = await request.update_datastore(None) assert response.identifier == expected_identity @@ -121,20 +121,20 @@ async def test_report_slave_id_request(self): } dif.get.return_value = identity - request = pymodbus_message.ReportSlaveIdRequest() + request = pymodbus_message.ReportDeviceIdRequest() response = await request.update_datastore(None) assert response.identifier == expected_identity - async def test_report_slave_id(self): - """Test report slave id.""" + async def test_report_device_id(self): + """Test report device_id.""" with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: dif.get.return_value = {} - request = pymodbus_message.ReportSlaveIdRequest() + request = pymodbus_message.ReportDeviceIdRequest() request.decode(b"\x12") assert not request.encode() assert (await request.update_datastore(None)).function_code == 0x11 - response = pymodbus_message.ReportSlaveIdResponse( + response = pymodbus_message.ReportDeviceIdResponse( (await request.update_datastore(None)).identifier, True ) diff --git a/test/pdu/test_pdu.py b/test/pdu/test_pdu.py index 111bdd974..86c699e2e 100644 --- a/test/pdu/test_pdu.py +++ b/test/pdu/test_pdu.py @@ -8,7 +8,7 @@ import pymodbus.pdu.other_message as o_msg import pymodbus.pdu.register_message as reg_msg from pymodbus.constants import ModbusStatus -from pymodbus.exceptions import NotImplementedException +from pymodbus.exceptions import ModbusIOException, NotImplementedException from pymodbus.pdu import ( ExceptionResponse, ModbusPDU, @@ -31,6 +31,11 @@ async def test_get_pdu_size(self): """Test get pdu size.""" assert not self.exception.get_response_pdu_size() + async def test_pdu_id(self): + """Test set illegal pdu id.""" + with pytest.raises(ModbusIOException): + ModbusPDU(256) + async def test_is_error(self): """Test is_error.""" assert self.exception.isError() @@ -85,11 +90,11 @@ def test_calculate_frame_size(self): (diag_msg.ReturnBusMessageCountRequest, (), {"message": 0x1010}, b'\x08\x00\x0b\x10\x10'), (diag_msg.ReturnBusCommunicationErrorCountRequest, (), {"message": 0x1010}, b'\x08\x00\x0c\x10\x10'), (diag_msg.ReturnBusExceptionErrorCountRequest, (), {"message": 0x1010}, b'\x08\x00\x0d\x10\x10'), - (diag_msg.ReturnSlaveMessageCountRequest, (), {"message": 0x1010}, b'\x08\x00\x0e\x10\x10'), - (diag_msg.ReturnSlaveNoResponseCountRequest, (), {"message": 0x1010}, b'\x08\x00\x0f\x10\x10'), - (diag_msg.ReturnSlaveNAKCountRequest, (), {"message": 0x1010}, b'\x08\x00\x10\x10\x10'), - (diag_msg.ReturnSlaveBusyCountRequest, (), {"message": 0x1010}, b'\x08\x00\x11\x10\x10'), - (diag_msg.ReturnSlaveBusCharacterOverrunCountRequest, (), {"message": 0x1010}, b'\x08\x00\x12\x10\x10'), + (diag_msg.ReturnDeviceMessageCountRequest, (), {"message": 0x1010}, b'\x08\x00\x0e\x10\x10'), + (diag_msg.ReturnDeviceNoResponseCountRequest, (), {"message": 0x1010}, b'\x08\x00\x0f\x10\x10'), + (diag_msg.ReturnDeviceNAKCountRequest, (), {"message": 0x1010}, b'\x08\x00\x10\x10\x10'), + (diag_msg.ReturnDeviceBusyCountRequest, (), {"message": 0x1010}, b'\x08\x00\x11\x10\x10'), + (diag_msg.ReturnDeviceBusCharacterOverrunCountRequest, (), {"message": 0x1010}, b'\x08\x00\x12\x10\x10'), (diag_msg.ReturnIopOverrunCountRequest, (), {"message": 0x1010}, b'\x08\x00\x13\x10\x10'), (diag_msg.ClearOverrunCountRequest, (), {"message": 0x1010}, b'\x08\x00\x14\x10\x10'), (diag_msg.GetClearModbusPlusRequest, (), {"message": 0x1010}, b'\x08\x00\x15\x10\x10'), @@ -100,7 +105,7 @@ def test_calculate_frame_size(self): (o_msg.ReadExceptionStatusRequest, (), {}, b'\x07'), (o_msg.GetCommEventCounterRequest, (), {}, b'\x0b'), (o_msg.GetCommEventLogRequest, (), {}, b'\x0c'), - (o_msg.ReportSlaveIdRequest, (), {}, b'\x11'), + (o_msg.ReportDeviceIdRequest, (), {}, b'\x11'), (reg_msg.ReadHoldingRegistersRequest, (), {"address": 117, "count": 3}, b'\x03\x00\x75\x00\x03'), (reg_msg.ReadInputRegistersRequest, (), {"address": 117, "count": 3}, b'\x04\x00\x75\x00\x03'), (reg_msg.ReadWriteMultipleRegistersRequest, (), {"read_address": 17, "read_count": 2, "write_address": 25, "write_registers": [111, 112]}, b'\x17\x00\x11\x00\x02\x00\x19\x00\x02\x04\x00\x6f\x00\x70'), @@ -125,11 +130,11 @@ def test_calculate_frame_size(self): (diag_msg.ReturnBusMessageCountResponse, (), {"message": 0x1010}, b'\x08\x00\x0b\x10\x10'), (diag_msg.ReturnBusCommunicationErrorCountResponse, (), {"message": 0x1010}, b'\x08\x00\x0c\x10\x10'), (diag_msg.ReturnBusExceptionErrorCountResponse, (), {"message": 0x1010}, b'\x08\x00\x0d\x10\x10'), - (diag_msg.ReturnSlaveMessageCountResponse, (), {"message": 0x1010}, b'\x08\x00\x0e\x10\x10'), - (diag_msg.ReturnSlaveNoResponseCountResponse, (), {"message": 0x1010}, b'\x08\x00\x0f\x10\x10'), - (diag_msg.ReturnSlaveNAKCountResponse, (), {"message": 0x1010}, b'\x08\x00\x10\x10\x10'), - (diag_msg.ReturnSlaveBusyCountResponse, (), {"message": 0x1010}, b'\x08\x00\x11\x10\x10'), - (diag_msg.ReturnSlaveBusCharacterOverrunCountResponse, (), {"message": 0x1010}, b'\x08\x00\x12\x10\x10'), + (diag_msg.ReturnDeviceMessageCountResponse, (), {"message": 0x1010}, b'\x08\x00\x0e\x10\x10'), + (diag_msg.ReturnDeviceNoResponseCountResponse, (), {"message": 0x1010}, b'\x08\x00\x0f\x10\x10'), + (diag_msg.ReturnDeviceNAKCountResponse, (), {"message": 0x1010}, b'\x08\x00\x10\x10\x10'), + (diag_msg.ReturnDeviceBusyCountResponse, (), {"message": 0x1010}, b'\x08\x00\x11\x10\x10'), + (diag_msg.ReturnDeviceBusCharacterOverrunCountResponse, (), {"message": 0x1010}, b'\x08\x00\x12\x10\x10'), (diag_msg.ReturnIopOverrunCountResponse, (), {"message": 0x1010}, b'\x08\x00\x13\x10\x10'), (diag_msg.ClearOverrunCountResponse, (), {"message": 0x1010}, b'\x08\x00\x14\x10\x10'), (diag_msg.GetClearModbusPlusResponse, (), {"message": 0x1010}, b'\x08\x00\x15\x10\x10'), @@ -140,7 +145,7 @@ def test_calculate_frame_size(self): (o_msg.ReadExceptionStatusResponse, (), {"status": 0x23}, b'\x07\x23'), (o_msg.GetCommEventCounterResponse, (), {"count": 123}, b'\x0b\x00\x00\x00\x7b'), (o_msg.GetCommEventLogResponse, (), {"status": True, "message_count": 12, "event_count": 7, "events": [12, 14]}, b'\x0c\x08\x00\x00\x00\x07\x00\x0c\x0c\x0e'), - (o_msg.ReportSlaveIdResponse, (), {"identifier": b'\x12', "status": True}, b'\x11\x02\x12\xff'), + (o_msg.ReportDeviceIdResponse, (), {"identifier": b'\x12', "status": True}, b'\x11\x02\x12\xff'), (reg_msg.ReadHoldingRegistersResponse, (), {"registers": [3, 17]}, b'\x03\x04\x00\x03\x00\x11'), (reg_msg.ReadInputRegistersResponse, (), {"registers": [3, 17]}, b'\x04\x04\x00\x03\x00\x11'), (reg_msg.ReadWriteMultipleRegistersResponse, (), {"registers": [1, 2]}, b'\x17\x04\x00\x01\x00\x02'), diff --git a/test/server/test_server_asyncio.py b/test/server/test_server_asyncio.py index eab736f0f..bbc83d454 100755 --- a/test/server/test_server_asyncio.py +++ b/test/server/test_server_asyncio.py @@ -8,15 +8,14 @@ import pytest -from pymodbus import FramerType +from pymodbus import FramerType, ModbusDeviceIdentification from pymodbus.client import AsyncModbusTcpClient from pymodbus.datastore import ( + ModbusDeviceContext, ModbusSequentialDataBlock, ModbusServerContext, - ModbusSlaveContext, ) -from pymodbus.device import ModbusDeviceIdentification -from pymodbus.exceptions import NoSuchSlaveException +from pymodbus.exceptions import NoSuchIdException from pymodbus.server import ModbusTcpServer, ModbusTlsServer, ModbusUdpServer @@ -112,13 +111,13 @@ class TestAsyncioServer: async def _setup_teardown(self): """Initialize the test environment by setting up a dummy store and context.""" self.loop = asyncio.get_running_loop() - self.store = ModbusSlaveContext( + self.store = ModbusDeviceContext( di=ModbusSequentialDataBlock(0, [17] * 100), co=ModbusSequentialDataBlock(0, [17] * 100), hr=ModbusSequentialDataBlock(0, [17] * 100), ir=ModbusSequentialDataBlock(0, [17] * 100), ) - self.context = ModbusServerContext(slaves=self.store, single=True) + self.context = ModbusServerContext(devices=self.store, single=True) self.identity = ModbusDeviceIdentification( info_name={"VendorName": "VendorName"} ) @@ -235,7 +234,7 @@ async def test_async_tcp_server_receive_data(self): async def test_async_tcp_server_roundtrip(self): """Test sending and receiving data on tcp socket.""" expected_response = b"\x01\x00\x00\x00\x00\x05\x01\x03\x02\x00\x11" - BasicClient.data = TEST_DATA # slave 1, read register + BasicClient.data = TEST_DATA # device 1, read register await self.start_server() await self.connect_server() await asyncio.wait_for(BasicClient.done, timeout=0.1) @@ -252,7 +251,7 @@ async def test_async_server_file_descriptors(self): for _ in range(2048): client = AsyncModbusTcpClient(addr[0], framer=FramerType.SOCKET, port=addr[1]) await client.connect() - response = await client.read_coils(31, count=1, slave=1) + response = await client.read_coils(31, count=1, device_id=1) assert not response.isError() client.close() @@ -274,10 +273,10 @@ async def test_async_tcp_server_shutdown_connection(self): await asyncio.sleep(0.5) await self.server.shutdown() - async def test_async_tcp_server_no_slave(self): - """Test unknown slave exception.""" + async def test_async_tcp_server_no_device(self): + """Test unknown device exception.""" self.context = ModbusServerContext( - slaves={0x01: self.store, 0x02: self.store}, single=False + devices={0x01: self.store, 0x02: self.store}, single=False ) BasicClient.data = b"\x01\x00\x00\x00\x00\x06\x05\x03\x00\x00\x00\x01" await self.start_server() @@ -292,7 +291,7 @@ async def test_async_tcp_server_modbus_error(self): await self.start_server() with mock.patch( "pymodbus.pdu.register_message.ReadHoldingRegistersRequest.update_datastore", - side_effect=NoSuchSlaveException, + side_effect=NoSuchIdException, ): await self.connect_server() await asyncio.wait_for(BasicClient.done, timeout=0.1) @@ -352,7 +351,7 @@ async def test_async_udp_server_roundtrip(self): expected_response = ( b"\x01\x00\x00\x00\x00\x05\x01\x03\x02\x00\x11" ) # value of 17 as per context - BasicClient.dataTo = TEST_DATA # slave 1, read register + BasicClient.dataTo = TEST_DATA # device 1, read register BasicClient.done = asyncio.Future() await self.start_server(do_udp=True) random_port = self.server.transport._sock.getsockname()[1] # pylint: disable=protected-access diff --git a/test/server/test_server_context.py b/test/server/test_server_context.py index 73e3b6247..802c991d1 100644 --- a/test/server/test_server_context.py +++ b/test/server/test_server_context.py @@ -1,24 +1,24 @@ """Test server context.""" import pytest -from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext -from pymodbus.exceptions import NoSuchSlaveException +from pymodbus.datastore import ModbusDeviceContext, ModbusServerContext +from pymodbus.exceptions import NoSuchIdException class TestServerSingleContext: - """This is the test for the pymodbus.datastore.ModbusServerContext using a single slave context.""" + """This is the test for the pymodbus.datastore.ModbusServerContext using a single device context.""" - slave = ModbusSlaveContext() + device = ModbusDeviceContext() context = None def setup_method(self): """Set up the test environment.""" - self.context = ModbusServerContext(slaves=self.slave, single=True) + self.context = ModbusServerContext(devices=self.device, single=True) def test_single_context_gets(self): """Test getting on a single context.""" for dev_id in range(0, 0xFF): - assert self.slave == self.context[dev_id] + assert self.device == self.context[dev_id] def test_single_context_deletes(self): """Test removing on multiple context.""" @@ -26,76 +26,76 @@ def test_single_context_deletes(self): def _test(): del self.context[0x00] - with pytest.raises(NoSuchSlaveException): + with pytest.raises(NoSuchIdException): _test() def test_single_context_iter(self): """Test iterating over a single context.""" - expected = (0, self.slave) - for slave in self.context: - assert slave == expected + expected = (0, self.device) + for device in self.context: + assert device == expected def test_single_context_default(self): """Test that the single context default values work.""" self.context = ModbusServerContext() - slave = self.context[0x00] - assert not slave + device = self.context[0x00] + assert not device def test_single_context_set(self): - """Test a setting a single slave context.""" - slave = ModbusSlaveContext() - self.context[0x00] = slave + """Test a setting a single device context.""" + device = ModbusDeviceContext() + self.context[0x00] = device actual = self.context[0x00] - assert slave == actual + assert device == actual def test_single_context_register(self): """Test single context register.""" request_db = [1, 2, 3] - slave = ModbusSlaveContext() - slave.register(0xFF, "custom_request", request_db) - assert slave.store["custom_request"] == request_db - assert slave.decode(0xFF) == "custom_request" + device = ModbusDeviceContext() + device.register(0xFF, "custom_request", request_db) + assert device.store["custom_request"] == request_db + assert device.decode(0xFF) == "custom_request" class TestServerMultipleContext: - """This is the test for the pymodbus.datastore.ModbusServerContext using multiple slave contexts.""" + """This is the test for the pymodbus.datastore.ModbusServerContext using multiple device contexts.""" - slaves = None + devices = None context = None def setup_method(self): """Set up the test environment.""" - self.slaves = {id: ModbusSlaveContext() for id in range(10)} - self.context = ModbusServerContext(slaves=self.slaves, single=False) + self.devices = {id: ModbusDeviceContext() for id in range(10)} + self.context = ModbusServerContext(devices=self.devices, single=False) def test_multiple_context_gets(self): """Test getting on multiple context.""" for dev_id in range(0, 10): - assert self.slaves[dev_id] == self.context[dev_id] + assert self.devices[dev_id] == self.context[dev_id] def test_multiple_context_deletes(self): """Test removing on multiple context.""" del self.context[0x00] - with pytest.raises(NoSuchSlaveException): + with pytest.raises(NoSuchIdException): self.context[0x00]() def test_multiple_context_iter(self): """Test iterating over multiple context.""" - for dev_id, slave in self.context: - assert slave == self.slaves[dev_id] + for dev_id, device in self.context: + assert device == self.devices[dev_id] assert dev_id in self.context def test_multiple_context_default(self): """Test that the multiple context default values work.""" self.context = ModbusServerContext(single=False) - with pytest.raises(NoSuchSlaveException): + with pytest.raises(NoSuchIdException): self.context[0x00]() def test_multiple_context_set(self): - """Test a setting multiple slave contexts.""" - slaves = {id: ModbusSlaveContext() for id in range(10)} - for dev_id, slave in iter(slaves.items()): - self.context[dev_id] = slave - for dev_id, slave in iter(slaves.items()): + """Test a setting multiple device contexts.""" + devices = {id: ModbusDeviceContext() for id in range(10)} + for dev_id, device in iter(devices.items()): + self.context[dev_id] = device + for dev_id, device in iter(devices.items()): actual = self.context[dev_id] - assert slave == actual + assert device == actual diff --git a/test/server/test_simulator.py b/test/server/test_simulator.py index d0b758ce7..569ebbd36 100644 --- a/test/server/test_simulator.py +++ b/test/server/test_simulator.py @@ -105,7 +105,7 @@ class TestSimulator: "comm": "tcp", "host": NULLMODEM_HOST, "port": 5020, - "ignore_missing_slaves": False, + "ignore_missing_devices": False, "framer": "socket", "identity": { "VendorName": "pymodbus", @@ -567,7 +567,7 @@ 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, count=1, slave=1) + result = await client.read_holding_registers(16, count=1, device_id=1) assert result.registers[0] == 3124 client.close() @@ -575,16 +575,16 @@ async def test_simulator_server_string(self, simulator_server, use_port): """Test simulator server end to end.""" client = AsyncModbusTcpClient(NULLMODEM_HOST, port=use_port) assert await client.connect() - result = await client.read_holding_registers(43, count=2, slave=1) + result = await client.read_holding_registers(43, count=2, device_id=1) assert result.registers[0] == int.from_bytes(bytes("St", "utf-8"), "big") assert result.registers[1] == int.from_bytes(bytes("r ", "utf-8"), "big") - result = await client.read_holding_registers(43, count=6, slave=1) + result = await client.read_holding_registers(43, count=6, device_id=1) assert result.registers[0] == int.from_bytes(bytes("St", "utf-8"), "big") assert result.registers[1] == int.from_bytes(bytes("r ", "utf-8"), "big") assert result.registers[2] == int.from_bytes(bytes("St", "utf-8"), "big") 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, count=23, slave=1) + result = await client.read_holding_registers(21, count=23, device_id=1) assert len(result.registers) == 23 client.close()