diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdb81a5d7..5f72094c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,7 +94,7 @@ jobs: run: pip install -e . -r requirements.txt - name: pytest - run: pytest -n0 --cov=pymodbus --cov=test --cov=examples --cov-report=term-missing --cov-report=xml -v --full-trace --timeout=20 + run: pytest -n0 -v --full-trace --timeout=20 analyze: name: Analyze Python diff --git a/doc/source/examples.rst b/doc/source/examples.rst index b918c5dc4..d7e749b55 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -54,10 +54,6 @@ Asynchronous server ^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../../examples/server_async.py -Build bcd Payload -^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../examples/build_bcd_payload.py - Callback Server example ^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../../examples/server_callback.py diff --git a/examples/build_bcd_payload.py b/examples/build_bcd_payload.py deleted file mode 100644 index 0479a6e4a..000000000 --- a/examples/build_bcd_payload.py +++ /dev/null @@ -1,222 +0,0 @@ -"""Modbus BCD Payload Builder. - -This is an example of building a custom payload builder -that can be used in the pymodbus library. Below is a -simple binary coded decimal builder and decoder. -""" - -from struct import pack - -from pymodbus.constants import Endian -from pymodbus.exceptions import ParameterException -from pymodbus.payload import BinaryPayloadDecoder -from pymodbus.utilities import pack_bitstring, unpack_bitstring - - -def convert_to_bcd(decimal: float) -> int: - """Convert a decimal value to a bcd value - - :param decimal: The decimal value to to pack into bcd - :returns: The number in bcd form - """ - place, bcd = 0, 0 - while decimal > 0: - nibble = decimal % 10 - bcd += nibble << place - decimal /= 10 - place += 4 - return bcd - - -def convert_from_bcd(bcd: int) -> int: - """Convert a bcd value to a decimal value - - :param bcd: The value to unpack from bcd - :returns: The number in decimal form - """ - place, decimal = 1, 0 - while bcd > 0: - nibble = bcd & 0xF - decimal += nibble * place - bcd >>= 4 - place *= 10 - return decimal - - -def count_bcd_digits(bcd: int) -> int: - """Count the number of digits in a bcd value - - :param bcd: The bcd number to count the digits of - :returns: The number of digits in the bcd string - """ - count = 0 - while bcd > 0: - count += 1 - bcd >>= 4 - return count - - -class BcdPayloadBuilder: - """A utility that helps build binary coded decimal payload messages - - to be written with the various modbus messages. - example:: - - builder = BcdPayloadBuilder() - builder.add_number(1) - builder.add_number(int(2.234 * 1000)) - payload = builder.build() - """ - - def __init__(self, payload=None, endian=Endian.Little): - """Initialize a new instance of the payload builder - - :param payload: Raw payload data to initialize with - :param endian: The endianness of the payload - """ - self._endian = endian - self._payload = payload or [] - - def __str__(self): - """Return the payload buffer as a string - - :returns: The payload buffer as a string - """ - return "".join(self._payload) - - def reset(self): - """Reset the payload buffer""" - self._payload = [] - - def build(self): - """Return the payload buffer as a list - - This list is two bytes per element and can - thus be treated as a list of registers. - - :returns: The payload buffer as a list - """ - string = str(self) - length = len(string) - string += "\x00" * (length % 2) - return [string[i : i + 2] for i in range(0, length, 2)] - - def add_bits(self, values: int) -> None: - """Add a collection of bits to be encoded - - If these are less than a multiple of eight, - they will be left padded with 0 bits to make - it so. - - :param values: The value to add to the buffer - """ - value = pack_bitstring(values) - self._payload.append(value) - - def add_number(self, value: int, size: int = None): - """Add any 8bit numeric type to the buffer - - :param value: The value to add to the buffer - :param size: Size of buffer - """ - encoded = [] - value = convert_to_bcd(value) - size = size or count_bcd_digits(value) - while size > 0: - nibble = value & 0xF - encoded.append(pack("B", nibble)) - value >>= 4 - size -= 1 - self._payload.extend(encoded) - - def add_string(self, value: str): - """Add a string to the buffer - - :param value: The value to add to the buffer - """ - self._payload.append(value) - - -class BcdPayloadDecoder: - """A utility that helps decode binary coded decimal payload messages from a modbus response message. - - What follows is a simple example:: - - decoder = BcdPayloadDecoder(payload) - first = decoder.decode_int(2) - second = decoder.decode_int(5) / 100 - """ - - def __init__(self, payload): - """Initialize a new payload decoder - - :param payload: The payload to decode with - """ - self._payload = payload - self._pointer = 0x00 - - @staticmethod - def fromRegisters(registers: int, endian: str = Endian.Little): - """Initialize a payload decoder - - with the result of reading a collection of registers from a modbus device. - - The registers are treated as a list of 2 byte values. - We have to do this because of how the data has already - been decoded by the rest of the library. - - :param registers: The register results to initialize with - :param endian: The endianness of the payload - :returns: An initialized PayloadDecoder - :raises ParameterException: parameter exception - """ - if isinstance(registers, list): # repack into flat binary - payload = "".join(pack(">H", x) for x in registers) - return BinaryPayloadDecoder(payload, endian) - raise ParameterException("Invalid collection of registers supplied") - - @staticmethod - def fromCoils(coils: int, endian: str = Endian.Little): - """Initialize a payload decoder. - - with the result of reading a collection of coils from a modbus device. - - The coils are treated as a list of bit(boolean) values. - - :param coils: The coil results to initialize with - :param endian: The endianness of the payload - :returns: An initialized PayloadDecoder - :raises ParameterException: parameter exception - """ - if isinstance(coils, list): - payload = pack_bitstring(coils) - return BinaryPayloadDecoder(payload, endian) - raise ParameterException("Invalid collection of coils supplied") - - def reset(self): - """Reset the decoder pointer back to the start""" - self._pointer = 0x00 - - def decode_int(self, size=1): - """Decode a int or long from the buffer""" - self._pointer += size - handle = self._payload[self._pointer - size : self._pointer] - return convert_from_bcd(handle) - - def decode_bits(self): - """Decode a byte worth of bits from the buffer""" - self._pointer += 1 - handle = self._payload[self._pointer - 1 : self._pointer] - return unpack_bitstring(handle) - - def decode_string(self, size: int = 1): - """Decode a string from the buffer - - :param size: The size of the string to decode - """ - self._pointer += size - return self._payload[self._pointer - size : self._pointer] - - -if __name__ == "__main__": - print("Test") diff --git a/examples/client_async.py b/examples/client_async.py index 396fd460d..6af8cde36 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -143,6 +143,11 @@ async def run_a_few_calls(client): pass +async def main(cmdline=None): + """Combine setup and run.""" + testclient = setup_async_client(description="Run client.", cmdline=cmdline) + await run_async_client(testclient, modbus_calls=run_a_few_calls) + + if __name__ == "__main__": - testclient = setup_async_client(description="Run asynchronous client.") - asyncio.run(run_async_client(testclient, modbus_calls=run_a_few_calls), debug=True) + asyncio.run(main(), debug=True) # pragma: no cover diff --git a/examples/client_async_calls.py b/examples/client_async_calls.py new file mode 100755 index 000000000..9e7ffce00 --- /dev/null +++ b/examples/client_async_calls.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Pymodbus Client modbus async call examples. + +Please see: + + async_template_call + +for a template on how to make modbus calls and check for different +error conditions. + +The handle_.... functions each handle a set of modbus calls with the +same register type (e.g. coils). + +All available modbus calls are present. The difference between async +and sync is a single 'await' so the calls are not repeated. + +If you are performing a request that is not available in the client +mixin, you have to perform the request like this instead: + +from pymodbus.diag_message import ClearCountersRequest +from pymodbus.diag_message import ClearCountersResponse + +request = ClearCountersRequest() +response = client.execute(request) +if isinstance(response, ClearCountersResponse): + ... do something with the response + +This example uses client_async.py and client_sync.py to handle connection, +and have the same options. + +The corresponding server must be started before e.g. as: + + ./server_async.py +""" +import asyncio +import logging + +import pymodbus.diag_message as req_diag +import pymodbus.mei_message as req_mei +import pymodbus.other_message as req_other +from examples.client_async import run_async_client, setup_async_client +from pymodbus.exceptions import ModbusException + + +logging.basicConfig() +_logger = logging.getLogger(__file__) +_logger.setLevel("DEBUG") + + +SLAVE = 0x01 + + +# -------------------------------------------------- +# Template on how to make modbus calls (sync/async). +# all calls follow the same schema, +# -------------------------------------------------- +async def async_template_call(client): + """Show complete modbus call, async version.""" + try: + rr = await client.read_coils(1, 1, slave=SLAVE) + except ModbusException as exc: + txt = f"ERROR: exception in pymodbus {exc}" + _logger.error(txt) + raise exc + if rr.isError(): + txt = "ERROR: pymodbus returned an error!" + _logger.error(txt) + raise ModbusException(txt) + + # Validate data + txt = f"### Template coils response: {rr.bits!s}" + _logger.debug(txt) + + +# ------------------------------------------------------ +# Call modbus device (all possible calls are presented). +# ------------------------------------------------------ +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, 1, slave=SLAVE) + assert not rr.isError() # test that call was OK + assert len(rr.bits) == 8 + + rr = await client.read_coils(1, 5, slave=SLAVE) + assert not rr.isError() # test that call was OK + assert len(rr.bits) == 8 + + rr = await client.read_coils(1, 12, slave=SLAVE) + assert not rr.isError() # test that call was OK + assert len(rr.bits) == 16 + + rr = await client.read_coils(1, 17, slave=SLAVE) + 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, 1, slave=SLAVE) + 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, 21, slave=SLAVE) + assert not rr.isError() # test that call was OK + resp = [True] * 21 + # If the returned output quantity is not a multiple of eight, + # the remaining bits in the final data byte will be padded with zeros + # (toward the high order end of the byte). + resp.extend([False] * 3) + 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, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK + assert rr.bits == [False] * 8 # test the expected value + + +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, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK + assert len(rr.bits) == 8 + + +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, 1, slave=SLAVE) + 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, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK + assert rr.registers == [10] * 8 + + _logger.info("### write read holding registers, using **kwargs") + arguments = { + "read_address": 1, + "read_count": 8, + "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, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK + assert rr.registers == arguments["values"] + + +async def async_handle_input_registers(client): + """Read input registers.""" + _logger.info("### read input registers") + rr = await client.read_input_registers(1, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK + assert len(rr.registers) == 8 + + +async def async_execute_information_requests(client): + """Execute extended information requests.""" + _logger.info("### Running information requests.") + rr = await client.execute(req_mei.ReadDeviceInformationRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK + assert rr.information[0] == b"Pymodbus" + + rr = await client.execute(req_other.ReportSlaveIdRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK + # assert rr.status + + rr = await client.execute(req_other.ReadExceptionStatusRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK + # assert not rr.status + + rr = await client.execute(req_other.GetCommEventCounterRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK + # assert rr.status + # assert not rr.count + + rr = await client.execute(req_other.GetCommEventLogRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK + # assert rr.status + # assert not (rr.event_count + rr.message_count + len(rr.events)) + + +async def async_execute_diagnostic_requests(client): + """Execute extended diagnostic requests.""" + _logger.info("### Running diagnostic requests.") + message = b"OK" + rr = await client.execute( + req_diag.ReturnQueryDataRequest(message=message, slave=SLAVE) + ) + assert not rr.isError() # test that call was OK + assert rr.message == message + + await client.execute(req_diag.RestartCommunicationsOptionRequest(slave=SLAVE)) + await client.execute(req_diag.ReturnDiagnosticRegisterRequest(slave=SLAVE)) + await client.execute(req_diag.ChangeAsciiInputDelimiterRequest(slave=SLAVE)) + + # NOT WORKING: _check_call(await client.execute(req_diag.ForceListenOnlyModeRequest(slave=SLAVE))) + # does not send a response + + await client.execute(req_diag.ClearCountersRequest()) + await client.execute(req_diag.ReturnBusCommunicationErrorCountRequest(slave=SLAVE)) + await client.execute(req_diag.ReturnBusExceptionErrorCountRequest(slave=SLAVE)) + await client.execute(req_diag.ReturnSlaveMessageCountRequest(slave=SLAVE)) + await client.execute(req_diag.ReturnSlaveNoResponseCountRequest(slave=SLAVE)) + await client.execute(req_diag.ReturnSlaveNAKCountRequest(slave=SLAVE)) + await client.execute(req_diag.ReturnSlaveBusyCountRequest(slave=SLAVE)) + await client.execute( + req_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=SLAVE) + ) + await client.execute(req_diag.ReturnIopOverrunCountRequest(slave=SLAVE)) + await client.execute(req_diag.ClearOverrunCountRequest(slave=SLAVE)) + # NOT WORKING _check_call(await client.execute(req_diag.GetClearModbusPlusRequest(slave=SLAVE))) + + +# ------------------------ +# Run the calls in groups. +# ------------------------ +async def run_async_calls(client): + """Demonstrate basic read/write calls.""" + await async_template_call(client) + await async_handle_coils(client) + await async_handle_discrete_input(client) + await async_handle_holding_registers(client) + await async_handle_input_registers(client) + await async_execute_information_requests(client) + await async_execute_diagnostic_requests(client) + + +async def main(cmdline=None): + """Combine setup and run""" + testclient = setup_async_client( + description="Run asynchronous client.", cmdline=cmdline + ) + await run_async_client(testclient, modbus_calls=run_async_calls) + + +if __name__ == "__main__": + asyncio.run(main()) # pragma: no cover diff --git a/examples/client_calls.py b/examples/client_calls.py index 7b8648013..90dcb6358 100755 --- a/examples/client_calls.py +++ b/examples/client_calls.py @@ -3,14 +3,12 @@ Please see: - async_template_call - template_call for a template on how to make modbus calls and check for different error conditions. -The _handle_.... functions each handle a set of modbus calls with the +The handle_.... functions each handle a set of modbus calls with the same register type (e.g. coils). All available modbus calls are present. The difference between async @@ -34,16 +32,13 @@ ./server_async.py """ -import asyncio import logging import pymodbus.diag_message as req_diag import pymodbus.mei_message as req_mei import pymodbus.other_message as req_other -from examples.client_async import run_async_client, setup_async_client from examples.client_sync import run_sync_client, setup_sync_client from pymodbus.exceptions import ModbusException -from pymodbus.pdu import ExceptionResponse logging.basicConfig() @@ -58,31 +53,6 @@ # Template on how to make modbus calls (sync/async). # all calls follow the same schema, # -------------------------------------------------- - - -async def async_template_call(client): - """Show complete modbus call, async version.""" - try: - rr = await client.read_coils(1, 1, slave=SLAVE) - except ModbusException as exc: - txt = f"ERROR: exception in pymodbus {exc}" - _logger.error(txt) - raise exc - if rr.isError(): - txt = "ERROR: pymodbus returned an error!" - _logger.error(txt) - raise ModbusException(txt) - if isinstance(rr, ExceptionResponse): - txt = "ERROR: received exception from device {rr}!" - _logger.error(txt) - # THIS IS NOT A PYTHON EXCEPTION, but a valid modbus message - raise ModbusException(txt) - - # Validate data - txt = f"### Template coils response: {rr.bits!s}" - _logger.debug(txt) - - def template_call(client): """Show complete modbus call, sync version.""" try: @@ -95,54 +65,43 @@ def template_call(client): txt = "ERROR: pymodbus returned an error!" _logger.error(txt) raise ModbusException(txt) - if isinstance(rr, ExceptionResponse): - txt = "ERROR: received exception from device {rr}!" - _logger.error(txt) - # THIS IS NOT A PYTHON EXCEPTION, but a valid modbus message - raise ModbusException(txt) # Validate data txt = f"### Template coils response: {rr.bits!s}" _logger.debug(txt) -# ------------------------------------------------- -# Generic error handling, to avoid duplicating code -# ------------------------------------------------- - - -def _check_call(rr): - """Check modbus call worked generically.""" - assert not rr.isError() # test that call was OK - assert not isinstance(rr, ExceptionResponse) # Device rejected request - return rr - - # ------------------------------------------------------ # Call modbus device (all possible calls are presented). # ------------------------------------------------------ -async def _handle_coils(client): +def handle_coils(client): """Read/Write coils.""" _logger.info("### Reading Coil different number of bits (return 8 bits multiples)") - rr = _check_call(await client.read_coils(1, 1, slave=SLAVE)) + rr = client.read_coils(1, 1, slave=SLAVE) + assert not rr.isError() # test that call was OK assert len(rr.bits) == 8 - rr = _check_call(await client.read_coils(1, 5, slave=SLAVE)) + rr = client.read_coils(1, 5, slave=SLAVE) + assert not rr.isError() # test that call was OK assert len(rr.bits) == 8 - rr = _check_call(await client.read_coils(1, 12, slave=SLAVE)) + rr = client.read_coils(1, 12, slave=SLAVE) + assert not rr.isError() # test that call was OK assert len(rr.bits) == 16 - rr = _check_call(await client.read_coils(1, 17, slave=SLAVE)) + rr = client.read_coils(1, 17, slave=SLAVE) + 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") - _check_call(await client.write_coil(0, True, slave=SLAVE)) - rr = _check_call(await client.read_coils(0, 1, slave=SLAVE)) + client.write_coil(0, True, slave=SLAVE) + rr = client.read_coils(0, 1, slave=SLAVE) + assert not rr.isError() # test that call was OK assert rr.bits[0] # test the expected value - _check_call(await client.write_coils(1, [True] * 21, slave=SLAVE)) - rr = _check_call(await client.read_coils(1, 21, slave=SLAVE)) + client.write_coils(1, [True] * 21, slave=SLAVE) + rr = client.read_coils(1, 21, slave=SLAVE) + assert not rr.isError() # test that call was OK resp = [True] * 21 # If the returned output quantity is not a multiple of eight, # the remaining bits in the final data byte will be padded with zeros @@ -151,27 +110,31 @@ async def _handle_coils(client): assert rr.bits == resp # test the expected value _logger.info("### Write False to address 1-8 coils") - _check_call(await client.write_coils(1, [False] * 8, slave=SLAVE)) - rr = _check_call(await client.read_coils(1, 8, slave=SLAVE)) + client.write_coils(1, [False] * 8, slave=SLAVE) + rr = client.read_coils(1, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK assert rr.bits == [False] * 8 # test the expected value -async def _handle_discrete_input(client): +def handle_discrete_input(client): """Read discrete inputs.""" _logger.info("### Reading discrete input, Read address:0-7") - rr = _check_call(await client.read_discrete_inputs(0, 8, slave=SLAVE)) + rr = client.read_discrete_inputs(0, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK assert len(rr.bits) == 8 -async def _handle_holding_registers(client): +def handle_holding_registers(client): """Read/write holding registers.""" _logger.info("### write holding register and read holding registers") - _check_call(await client.write_register(1, 10, slave=SLAVE)) - rr = _check_call(await client.read_holding_registers(1, 1, slave=SLAVE)) + client.write_register(1, 10, slave=SLAVE) + rr = client.read_holding_registers(1, 1, slave=SLAVE) + assert not rr.isError() # test that call was OK assert rr.registers[0] == 10 - _check_call(await client.write_registers(1, [10] * 8, slave=SLAVE)) - rr = _check_call(await client.read_holding_registers(1, 8, slave=SLAVE)) + client.write_registers(1, [10] * 8, slave=SLAVE) + rr = client.read_holding_registers(1, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK assert rr.registers == [10] * 8 _logger.info("### write read holding registers, using **kwargs") @@ -181,130 +144,93 @@ async def _handle_holding_registers(client): "write_address": 1, "values": [256, 128, 100, 50, 25, 10, 5, 1], } - _check_call(await client.readwrite_registers(slave=SLAVE, **arguments)) - rr = _check_call(await client.read_holding_registers(1, 8, slave=SLAVE)) + client.readwrite_registers(slave=SLAVE, **arguments) + rr = client.read_holding_registers(1, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK assert rr.registers == arguments["values"] -async def _handle_input_registers(client): +def handle_input_registers(client): """Read input registers.""" _logger.info("### read input registers") - rr = _check_call(await client.read_input_registers(1, 8, slave=SLAVE)) + rr = client.read_input_registers(1, 8, slave=SLAVE) + assert not rr.isError() # test that call was OK assert len(rr.registers) == 8 -async def _execute_information_requests(client): +def execute_information_requests(client): # pragma no cover """Execute extended information requests.""" _logger.info("### Running information requests.") - rr = _check_call( - await client.execute(req_mei.ReadDeviceInformationRequest(slave=SLAVE)) - ) + rr = client.execute(req_mei.ReadDeviceInformationRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK assert rr.information[0] == b"Pymodbus" - rr = _check_call(await client.execute(req_other.ReportSlaveIdRequest(slave=SLAVE))) + rr = client.execute(req_other.ReportSlaveIdRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK # assert rr.status - rr = _check_call( - await client.execute(req_other.ReadExceptionStatusRequest(slave=SLAVE)) - ) + rr = client.execute(req_other.ReadExceptionStatusRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK # assert not rr.status - rr = _check_call( - await client.execute(req_other.GetCommEventCounterRequest(slave=SLAVE)) - ) + rr = client.execute(req_other.GetCommEventCounterRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK # assert rr.status # assert not rr.count - rr = _check_call( - await client.execute(req_other.GetCommEventLogRequest(slave=SLAVE)) - ) + rr = client.execute(req_other.GetCommEventLogRequest(slave=SLAVE)) + assert not rr.isError() # test that call was OK # assert rr.status # assert not (rr.event_count + rr.message_count + len(rr.events)) -async def _execute_diagnostic_requests(client): +def execute_diagnostic_requests(client): # pragma no cover """Execute extended diagnostic requests.""" _logger.info("### Running diagnostic requests.") message = b"OK" - rr = _check_call( - await client.execute( - req_diag.ReturnQueryDataRequest(message=message, slave=SLAVE) - ) - ) + rr = client.execute(req_diag.ReturnQueryDataRequest(message=message, slave=SLAVE)) + assert not rr.isError() # test that call was OK assert rr.message == message - _check_call( - await client.execute(req_diag.RestartCommunicationsOptionRequest(slave=SLAVE)) - ) - _check_call( - await client.execute(req_diag.ReturnDiagnosticRegisterRequest(slave=SLAVE)) - ) - _check_call( - await client.execute(req_diag.ChangeAsciiInputDelimiterRequest(slave=SLAVE)) - ) - - # NOT WORKING: _check_call(await client.execute(req_diag.ForceListenOnlyModeRequest(slave=SLAVE))) + client.execute(req_diag.RestartCommunicationsOptionRequest(slave=SLAVE)) + client.execute(req_diag.ReturnDiagnosticRegisterRequest(slave=SLAVE)) + client.execute(req_diag.ChangeAsciiInputDelimiterRequest(slave=SLAVE)) + + # NOT WORKING: _check_call(client.execute(req_diag.ForceListenOnlyModeRequest(slave=SLAVE))) # does not send a response - _check_call(await client.execute(req_diag.ClearCountersRequest())) - _check_call( - await client.execute( - req_diag.ReturnBusCommunicationErrorCountRequest(slave=SLAVE) - ) - ) - _check_call( - await client.execute(req_diag.ReturnBusExceptionErrorCountRequest(slave=SLAVE)) - ) - _check_call( - await client.execute(req_diag.ReturnSlaveMessageCountRequest(slave=SLAVE)) - ) - _check_call( - await client.execute(req_diag.ReturnSlaveNoResponseCountRequest(slave=SLAVE)) - ) - _check_call(await client.execute(req_diag.ReturnSlaveNAKCountRequest(slave=SLAVE))) - _check_call(await client.execute(req_diag.ReturnSlaveBusyCountRequest(slave=SLAVE))) - _check_call( - await client.execute( - req_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=SLAVE) - ) - ) - _check_call( - await client.execute(req_diag.ReturnIopOverrunCountRequest(slave=SLAVE)) - ) - _check_call(await client.execute(req_diag.ClearOverrunCountRequest(slave=SLAVE))) - # NOT WORKING _check_call(await client.execute(req_diag.GetClearModbusPlusRequest(slave=SLAVE))) + client.execute(req_diag.ClearCountersRequest()) + client.execute(req_diag.ReturnBusCommunicationErrorCountRequest(slave=SLAVE)) + client.execute(req_diag.ReturnBusExceptionErrorCountRequest(slave=SLAVE)) + client.execute(req_diag.ReturnSlaveMessageCountRequest(slave=SLAVE)) + client.execute(req_diag.ReturnSlaveNoResponseCountRequest(slave=SLAVE)) + client.execute(req_diag.ReturnSlaveNAKCountRequest(slave=SLAVE)) + client.execute(req_diag.ReturnSlaveBusyCountRequest(slave=SLAVE)) + client.execute(req_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=SLAVE)) + client.execute(req_diag.ReturnIopOverrunCountRequest(slave=SLAVE)) + client.execute(req_diag.ClearOverrunCountRequest(slave=SLAVE)) + # NOT WORKING _check_call(client.execute(req_diag.GetClearModbusPlusRequest(slave=SLAVE))) # ------------------------ # Run the calls in groups. # ------------------------ - - -async def run_async_calls(client): - """Demonstrate basic read/write calls.""" - await async_template_call(client) - await _handle_coils(client) - await _handle_discrete_input(client) - await _handle_holding_registers(client) - await _handle_input_registers(client) - await _execute_information_requests(client) - await _execute_diagnostic_requests(client) - - def run_sync_calls(client): """Demonstrate basic read/write calls.""" template_call(client) + handle_coils(client) + handle_discrete_input(client) + handle_holding_registers(client) + handle_input_registers(client) + # awaiting fix: execute_information_requests(client) + # awaiting fix: execute_diagnostic_requests(client) -async def async_helper(): - """Combine the setup and run""" - testclient = setup_async_client(description="Run asynchronous client.") - await run_async_client(testclient, modbus_calls=run_async_calls) +def main(cmdline=None): + """Combine setup and run.""" + client = setup_sync_client(description="Run synchronous client.", cmdline=cmdline) + run_sync_client(client, modbus_calls=run_sync_calls) if __name__ == "__main__": - asyncio.run(async_helper()) - testclient = setup_sync_client( - description="Run modbus calls in synchronous client." - ) - run_sync_client(testclient, modbus_calls=run_sync_calls) + main() # pragma: no cover diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index b8e575539..e99f4b960 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -9,16 +9,18 @@ result = client.read_coils(1,10) print result """ +import asyncio import logging import struct from pymodbus.bit_read_message import ReadCoilsRequest -from pymodbus.client import ModbusTcpClient as ModbusClient +from pymodbus.client import AsyncModbusTcpClient as ModbusClient # --------------------------------------------------------------------------- # # import the various server implementations # --------------------------------------------------------------------------- # from pymodbus.pdu import ModbusExceptions, ModbusRequest, ModbusResponse +from pymodbus.transaction import ModbusSocketFramer # --------------------------------------------------------------------------- # @@ -39,7 +41,7 @@ # --------------------------------------------------------------------------- # -class CustomModbusResponse(ModbusResponse): +class CustomModbusResponse(ModbusResponse): # pragma no cover """Custom modbus response.""" function_code = 55 @@ -87,11 +89,11 @@ def encode(self): """Encode.""" return struct.pack(">HH", self.address, self.count) - def decode(self, data): + def decode(self, data): # pragma no cover """Decode.""" self.address, self.count = struct.unpack(">HH", data) - def execute(self, context): + def execute(self, context): # pragma no cover """Execute.""" if not 1 <= self.count <= 0x7D0: return self.doException(ModbusExceptions.IllegalValue) @@ -125,25 +127,22 @@ def __init__(self, address, **kwargs): # --------------------------------------------------------------------------- # -def run_custom_client(host, port): +async def main(host="localhost", port=5020): """Run versions of read coil.""" - with ModbusClient(host=host, port=port) as client: - # Standard call - request = ReadCoilsRequest(32, 1, slave=1) - result = client.execute(request) - print(result) - - # inherited request - request = Read16CoilsRequest(32, slave=1) - result = client.execute(request) - print(result) + with ModbusClient(host=host, port=port, framer=ModbusSocketFramer) as client: + await client.connect() # new modbus function code. client.register(CustomModbusResponse) request = CustomModbusRequest(32, slave=1) - result = client.execute(request) + result = await client.execute(request) + print(result) + + # inherited request + request = Read16CoilsRequest(32, slave=1) + result = await client.execute(request) print(result) if __name__ == "__main__": - run_custom_client("localhost", "5020") + asyncio.run(main()) # pragma: no cover diff --git a/examples/client_payload.py b/examples/client_payload.py index 194f788d8..31de2a1f0 100755 --- a/examples/client_payload.py +++ b/examples/client_payload.py @@ -137,11 +137,11 @@ async def run_payload_calls(client): print("\n") -async def async_helper(): +async def main(cmdline=None): """Combine the setup and run""" - testclient = setup_async_client(description="Run asynchronous client.") - await run_async_client(testclient, modbus_calls=run_payload_calls) + client = setup_async_client(description="Run asynchronous client.", cmdline=cmdline) + await run_async_client(client, modbus_calls=run_payload_calls) if __name__ == "__main__": - asyncio.run(async_helper()) + asyncio.run(main()) # pragma: no cover diff --git a/examples/client_sync.py b/examples/client_sync.py index 9cfc26410..bf4c4b136 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -97,7 +97,7 @@ def setup_sync_client(description=None, cmdline=None): # stopbits=1, # handle_local_echo=False, ) - elif args.comm == "tls": + elif args.comm == "tls": # pragma no cover client = ModbusTlsClient( args.host, port=args.port, @@ -137,6 +137,13 @@ def run_a_few_calls(client): assert rr.registers[1] == 17 -if __name__ == "__main__": - testclient = setup_sync_client(description="Run synchronous client.") +def main(cmdline=None): + """Combine setup and run.""" + testclient = setup_sync_client( + description="Run synchronous client.", cmdline=cmdline + ) run_sync_client(testclient, modbus_calls=run_a_few_calls) + + +if __name__ == "__main__": + main() # pragma: no cover diff --git a/examples/client_sync_test.py b/examples/client_sync_test.py deleted file mode 100755 index f068a0d3d..000000000 --- a/examples/client_sync_test.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -"""Pymodbus Synchronous Client Example. - -An example of a single threaded synchronous client. - -usage: client_sync.py [-h] [--comm {tcp,udp,serial,tls}] - [--framer {ascii,binary,rtu,socket,tls}] - [--log {critical,error,warning,info,debug}] - [--port PORT] -options: - -h, --help show this help message and exit - --comm {tcp,udp,serial,tls} - "serial", "tcp", "udp" or "tls" - --framer {ascii,binary,rtu,socket,tls} - "ascii", "binary", "rtu", "socket" or "tls" - --log {critical,error,warning,info,debug} - "critical", "error", "warning", "info" or "debug" - --port PORT the port to use - --baudrate BAUDRATE the baud rate to use for the serial device - -The corresponding server must be started before e.g. as: - python3 server_sync.py -""" -import logging - -import pymodbus.bit_read_message as pdu_bit_read -import pymodbus.bit_write_message as pdu_bit_write - -# --------------------------------------------------------------------------- # -# import the various client implementations -# --------------------------------------------------------------------------- # -from examples import helper -from pymodbus.client import ModbusTcpClient -from pymodbus.exceptions import ModbusException - - -logging.basicConfig() -_logger = logging.getLogger(__file__) -_logger.setLevel("DEBUG") - - -def run_sync_client(): - """Run sync client.""" - args = helper.get_commandline(server=False) - _logger.info("### Create client object") - client = ModbusTcpClient( - args.host, - port=args.port, - framer=args.framer, - ) - - _logger.info("### Client starting") - client.connect() - - _logger.info("### first read_coils to secure it works generally") - try: - rr = client.read_coils(1, 1, slave=1) - except ModbusException as exc: - _logger.error(exc) - raise RuntimeError(exc) from exc - if rr.isError(): - raise RuntimeError("ERROR: read_coils returned an error!") - assert isinstance(rr, pdu_bit_read.ReadCoilsResponse) - - _logger.info("### next write_coil to change a value") - try: - rr = client.write_coil(1, 17, slave=1) - except ModbusException as exc: - _logger.error(exc) - raise RuntimeError(exc) from exc - if rr.isError(): - raise RuntimeError("ERROR: write_coil returned an error!") - assert isinstance(rr, pdu_bit_write.WriteSingleCoilResponse) - - _logger.info("### finally read_coil to verify value") - try: - rr = client.read_coils(1, 1, slave=1) - except ModbusException as exc: - _logger.error(exc) - raise RuntimeError(exc) from exc - if rr.isError(): - raise RuntimeError("ERROR: read_coils(2) returned an error!") - assert isinstance(rr, pdu_bit_read.ReadCoilsResponse) - - client.close() - _logger.info("### End of Program") - - -if __name__ == "__main__": - run_sync_client() diff --git a/examples/datastore_simulator.py b/examples/datastore_simulator.py index eec1bc055..34036af03 100755 --- a/examples/datastore_simulator.py +++ b/examples/datastore_simulator.py @@ -173,6 +173,11 @@ async def run_server_simulator(args): ) +async def main(cmdline=None): + """Combine setup and run""" + run_args = setup_simulator(cmdline=cmdline) + await run_server_simulator(run_args) + + if __name__ == "__main__": - run_args = setup_simulator() - asyncio.run(run_server_simulator(run_args), debug=True) + asyncio.run(main(), debug=True) # pragma: no cover diff --git a/examples/helper.py b/examples/helper.py index ea19b79fe..6e6f226b1 100755 --- a/examples/helper.py +++ b/examples/helper.py @@ -90,7 +90,7 @@ def get_commandline(server=False, description=None, extras=None, cmdline=None): help="ADVANCED USAGE: set datastore context object", default=None, ) - if extras: + if extras: # pragma no cover for extra in extras: parser.add_argument(extra[0], **extra[1]) args = parser.parse_args(cmdline) @@ -125,13 +125,13 @@ def get_certificate(suffix: str): delimiter = "\\" if os.name == "nt" else "/" cwd = os.getcwd().split(delimiter)[-1] if cwd == "examples": - path = "." + path = "." # pragma no cover elif cwd == "sub_examples": - path = "../../examples" + path = "../../examples" # pragma no cover elif cwd == "test": path = "../examples" elif cwd == "pymodbus": - path = "examples" + path = "examples" # pragma no cover else: raise RuntimeError(f"**Error** Cannot find certificate path={cwd}") return f"{path}/certificates/pymodbus.{suffix}" diff --git a/examples/message_generator.py b/examples/message_generator.py index 0c3e96a0e..e381a7290 100755 --- a/examples/message_generator.py +++ b/examples/message_generator.py @@ -216,4 +216,4 @@ def generate_messages(cmdline=None): if __name__ == "__main__": - generate_messages() + generate_messages() # pragma: no cover diff --git a/examples/message_parser.py b/examples/message_parser.py index 12c72bb6f..574c16efd 100755 --- a/examples/message_parser.py +++ b/examples/message_parser.py @@ -82,13 +82,13 @@ def decode(self, message): print("-" * 80) try: decoder.addToFrame(message) - if decoder.checkFrame(): + if decoder.checkFrame(): # pragma no cover slave = decoder._header.get( # pylint: disable=protected-access "uid", 0x00 ) decoder.advanceFrame() decoder.processIncomingPacket(message, self.report, slave) - else: + else: # pragma no cover self.check_errors(decoder, message) except Exception: # pylint: disable=broad-except self.check_errors(decoder, message) @@ -98,7 +98,7 @@ def check_errors(self, decoder, message): txt = f"Unable to parse message - {message} with {decoder}" _logger.error(txt) - def report(self, message): + def report(self, message): # pragma no cover """Print the message information""" print( "%-15s = %s" # pylint: disable=consider-using-f-string @@ -146,7 +146,7 @@ def parse_messages(cmdline=None): """Do a helper method to generate the messages to parse""" args = get_commandline(cmdline=cmdline) _logger.setLevel(args.log.upper()) - if not args.message: + if not args.message: # pragma no cover _logger.error("Missing --message.") return @@ -162,5 +162,10 @@ def parse_messages(cmdline=None): decoder.decode(raw_message) +def main(cmdline=None): + """Run program""" + parse_messages(cmdline=cmdline) + + if __name__ == "__main__": - parse_messages() + main() # pragma: no cover diff --git a/examples/modbus_forwarder.py b/examples/modbus_forwarder.py index d2a88ddd7..9398adcf4 100755 --- a/examples/modbus_forwarder.py +++ b/examples/modbus_forwarder.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# pragma no cover """Pymodbus synchronous forwarder. This is a repeater or converter and an example of just how powerful datastore is. @@ -63,7 +64,8 @@ async def run_forwarder(args): # loop forever -if __name__ == "__main__": +async def async_helper(): + """Combine setup and run""" cmd_args = helper.get_commandline( server=True, description="Run asynchronous forwarder.", @@ -77,4 +79,8 @@ async def run_forwarder(args): ) ], ) - asyncio.run(run_forwarder(cmd_args)) + await run_forwarder(cmd_args) + + +if __name__ == "__main__": + asyncio.run(async_helper()) # pragma: no cover diff --git a/examples/server_async.py b/examples/server_async.py index 0cc9b4e19..f1cb70191 100755 --- a/examples/server_async.py +++ b/examples/server_async.py @@ -221,7 +221,12 @@ async def run_async_server(args): return server -if __name__ == "__main__": +async def async_helper(): + """Combine setup and run.""" _logger.info("Starting...") run_args = setup_server(description="Run asynchronous server.") - asyncio.run(run_async_server(run_args), debug=True) + await run_async_server(run_args) + + +if __name__ == "__main__": + asyncio.run(async_helper(), debug=True) # pragma: no cover diff --git a/examples/server_callback.py b/examples/server_callback.py index dbcb5cb2a..fd7fffffe 100755 --- a/examples/server_callback.py +++ b/examples/server_callback.py @@ -55,6 +55,7 @@ async def run_callback_server(cmdline=None): """Define datastore callback for server and do setup.""" 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) run_args = setup_server( @@ -64,4 +65,4 @@ async def run_callback_server(cmdline=None): if __name__ == "__main__": - asyncio.run(run_callback_server()) + asyncio.run(run_callback_server(), debug=True) # pragma: no cover diff --git a/examples/server_payload.py b/examples/server_payload.py index 6a97d5711..e2433b0c0 100755 --- a/examples/server_payload.py +++ b/examples/server_payload.py @@ -58,6 +58,11 @@ def setup_payload_server(cmdline=None): ) +async def main(cmdline=None): + """Combine setup and run""" + run_args = setup_payload_server(cmdline=cmdline) + await run_async_server(run_args) + + if __name__ == "__main__": - run_args = setup_payload_server() - asyncio.run(run_async_server(run_args)) + asyncio.run(main(), debug=True) # pragma: no cover diff --git a/examples/server_sync.py b/examples/server_sync.py index 0e880b507..faae1fca4 100755 --- a/examples/server_sync.py +++ b/examples/server_sync.py @@ -103,7 +103,7 @@ def run_sync_server(args): # broadcast_enable=False, # treat slave_id 0 as broadcast address, # strict=True, # use strict timing, t1.5 for Modbus RTU ) - elif args.comm == "tls": + elif args.comm == "tls": # pragma no cover address = ("", args.port) if args.port else None server = StartTlsServer( context=args.context, # Data storage @@ -129,7 +129,12 @@ def run_sync_server(args): return server -if __name__ == "__main__": +def sync_helper(): + """Combine setup and run.""" run_args = setup_server(description="Run synchronous server.") server = run_sync_server(run_args) server.shutdown() + + +if __name__ == "__main__": + sync_helper() # pragma: no cover diff --git a/examples/server_updating.py b/examples/server_updating.py index 0d2b5f24d..91ee4a2f0 100755 --- a/examples/server_updating.py +++ b/examples/server_updating.py @@ -80,6 +80,11 @@ async def run_updating_server(args): await run_async_server(args) +async def main(cmdline=None): + """Combine setup and run""" + run_args = setup_updating_server(cmdline=cmdline) + await run_updating_server(run_args) + + if __name__ == "__main__": - run_args = setup_updating_server() - asyncio.run(run_updating_server(run_args), debug=True) + asyncio.run(main(), debug=True) # pragma: no cover diff --git a/examples/simple_async_client.py b/examples/simple_async_client.py index 7d50db016..65306b9f4 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -33,17 +33,14 @@ ) -async def run_async_client(host, port): +async def run_async_simple_client(comm, host, port): """Run async client.""" # activate debugging pymodbus_apply_logging_config("DEBUG") - # change to test other client types - select_my_client = "tcp" - print("get client") - if select_my_client == "tcp": + if comm == "tcp": client = AsyncModbusTcpClient( host, port=port, @@ -55,7 +52,7 @@ async def run_async_client(host, port): # strict=True, # source_address=("localhost", 0), ) - elif select_my_client == "udp": + elif comm == "udp": client = AsyncModbusUdpClient( host, port=port, @@ -67,7 +64,7 @@ async def run_async_client(host, port): # strict=True, # source_address=None, ) - elif select_my_client == "serial": + elif comm == "serial": client = AsyncModbusSerialClient( port, framer=ModbusRtuFramer, @@ -77,12 +74,12 @@ async def run_async_client(host, port): # close_comm_on_error=False, # strict=True, baudrate=9600, - # bytesize=8, - # parity="N", - # stopbits=1, + bytesize=8, + parity="N", + stopbits=1, # handle_local_echo=False, ) - elif select_my_client == "tls": + elif comm == "tls": client = AsyncModbusTlsClient( host, port=port, @@ -93,13 +90,13 @@ async def run_async_client(host, port): # close_comm_on_error=False, # strict=True, # sslctx=sslctx, - certfile="my_cert.crt", - keyfile="my_cert.key", + certfile="../examples/certificates/pymodbus.crt", + keyfile="../examples/certificates/pymodbus.key", # password="none", server_hostname="localhost", ) - else: - print(f"Unknown client {select_my_client} selected") + else: # pragma no cover + print(f"Unknown client {comm} selected") return print("connect to server") @@ -111,15 +108,15 @@ async def run_async_client(host, port): try: # See all calls in client_calls.py rr = await client.read_coils(1, 1, slave=1) - except ModbusException as exc: + except ModbusException as exc: # pragma no cover print(f"Received ModbusException({exc}) from library") client.close() return - if rr.isError(): + if rr.isError(): # pragma no cover print(f"Received Modbus library error({rr})") client.close() return - if isinstance(rr, ExceptionResponse): + if isinstance(rr, ExceptionResponse): # pragma no cover print(f"Received Modbus library exception ({rr})") # THIS IS NOT A PYTHON EXCEPTION, but a valid modbus message client.close() @@ -129,4 +126,6 @@ async def run_async_client(host, port): if __name__ == "__main__": - asyncio.run(run_async_client("127.0.0.1", 5020), debug=True) + asyncio.run( + run_async_simple_client("tcp", "127.0.0.1", 5020), debug=True + ) # pragma: no cover diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index 1b09caf37..4ac99c12c 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -31,17 +31,14 @@ ) -def run_sync_client(host, port): +def run_sync_simple_client(comm, host, port): """Run sync client.""" # activate debugging pymodbus_apply_logging_config("DEBUG") - # change to test other client types - select_my_client = "tcp" - print("get client") - if select_my_client == "tcp": + if comm == "tcp": client = ModbusTcpClient( host, port=port, @@ -53,7 +50,7 @@ def run_sync_client(host, port): # strict=True, # source_address=("localhost", 0), ) - elif select_my_client == "tcp": + elif comm == "udp": client = ModbusUdpClient( host, port=port, @@ -65,7 +62,7 @@ def run_sync_client(host, port): # strict=True, # source_address=None, ) - elif select_my_client == "serial": + elif comm == "serial": client = ModbusSerialClient( port, framer=ModbusRtuFramer, @@ -75,12 +72,12 @@ def run_sync_client(host, port): # close_comm_on_error=False,. # strict=True, baudrate=9600, - # bytesize=8, - # parity="N", - # stopbits=1, + bytesize=8, + parity="N", + stopbits=1, # handle_local_echo=False, ) - elif select_my_client == "tls": + elif comm == "tls": client = ModbusTlsClient( host, port=port, @@ -91,13 +88,13 @@ def run_sync_client(host, port): # close_comm_on_error=False, # strict=True, # sslctx=None, - certfile="my_cert.crt", - keyfile="my_cert.key", + certfile="../examples/certificates/pymodbus.crt", + keyfile="../examples/certificates/pymodbus.key", # password=None, server_hostname="localhost", ) - else: - print(f"Unknown client {select_my_client} selected") + else: # pragma no cover + print(f"Unknown client {comm} selected") return print("connect to server") @@ -110,18 +107,18 @@ def run_sync_client(host, port): print(f"Received ModbusException({exc}) from library") client.close() return - if rr.isError(): + if rr.isError(): # pragma no cover print(f"Received Modbus library error({rr})") client.close() return - if isinstance(rr, ExceptionResponse): + if isinstance(rr, ExceptionResponse): # pragma no cover print(f"Received Modbus library exception ({rr})") # THIS IS NOT A PYTHON EXCEPTION, but a valid modbus message client.close() - print("close connection") - client.close() + print("close connection") # pragma no cover + client.close() # pragma no cover if __name__ == "__main__": - run_sync_client("127.0.0.1", "5020") + run_sync_simple_client("tcp", "127.0.0.1", "5020") # pragma: no cover diff --git a/examples/simulator.py b/examples/simulator.py index 820eaa415..34d67bad2 100755 --- a/examples/simulator.py +++ b/examples/simulator.py @@ -89,4 +89,4 @@ async def run_simulator(): if __name__ == "__main__": - asyncio.run(run_simulator(), debug=True) + asyncio.run(run_simulator(), debug=True) # pragma no cover diff --git a/test/sub_examples/test_client_server_async.py b/test/sub_examples/test_client_server_async.py index b517ca6be..d3ac55977 100755 --- a/test/sub_examples/test_client_server_async.py +++ b/test/sub_examples/test_client_server_async.py @@ -8,10 +8,17 @@ These are basis for most examples and thus tested separately """ import asyncio +from unittest import mock import pytest -from examples.client_async import run_a_few_calls, run_async_client, setup_async_client +from examples.client_async import ( + main, + run_a_few_calls, + run_async_client, + setup_async_client, +) +from pymodbus.exceptions import ModbusIOException BASE_PORT = 6200 @@ -37,9 +44,22 @@ class TestClientServerAsyncExamples: USE_CASES, ) async def test_combinations(self, mock_server, mock_clc): + """Run async client and server.""" + assert mock_server + await main(cmdline=mock_clc) + + @pytest.mark.parametrize("port_offset", [1]) + @pytest.mark.parametrize( + ("use_comm", "use_framer", "use_port"), + [("tcp", "socket", BASE_PORT + 1)], + ) + async def test_client_exception(self, mock_server, mock_clc): """Run async client and server.""" assert mock_server test_client = setup_async_client(cmdline=mock_clc) + test_client.read_holding_registers = mock.AsyncMock( + side_effect=ModbusIOException("test") + ) await run_async_client(test_client, modbus_calls=run_a_few_calls) @pytest.mark.parametrize("port_offset", [10]) diff --git a/test/sub_examples/test_client_server_sync.py b/test/sub_examples/test_client_server_sync.py index 784d93766..40debbdbe 100755 --- a/test/sub_examples/test_client_server_sync.py +++ b/test/sub_examples/test_client_server_sync.py @@ -12,7 +12,12 @@ import pytest -from examples.client_sync import run_a_few_calls, run_sync_client, setup_sync_client +from examples.client_sync import ( + main, + run_a_few_calls, + run_sync_client, + setup_sync_client, +) from examples.server_async import setup_server from examples.server_sync import run_sync_server from pymodbus.exceptions import ConnectionException @@ -53,8 +58,7 @@ def test_combinations( thread.daemon = True thread.start() sleep(1) - test_client = setup_sync_client(cmdline=mock_clc) - run_sync_client(test_client, modbus_calls=run_a_few_calls) + main(cmdline=mock_clc) ServerStop() @pytest.mark.parametrize("port_offset", [10]) diff --git a/test/sub_examples/test_examples.py b/test/sub_examples/test_examples.py index 5095d175f..605becacb 100755 --- a/test/sub_examples/test_examples.py +++ b/test/sub_examples/test_examples.py @@ -4,25 +4,33 @@ """ import asyncio +from threading import Thread +from time import sleep +from unittest import mock import pytest -from examples.build_bcd_payload import BcdPayloadBuilder, BcdPayloadDecoder from examples.client_async import run_a_few_calls, run_async_client, setup_async_client -from examples.client_calls import run_async_calls -from examples.client_custom_msg import run_custom_client -from examples.client_payload import run_payload_calls -from examples.datastore_simulator import run_server_simulator, setup_simulator +from examples.client_async_calls import async_template_call +from examples.client_async_calls import main as main_client_async_calls +from examples.client_calls import main as main_client_calls +from examples.client_calls import template_call +from examples.client_custom_msg import main as main_custom_client +from examples.client_payload import main as main_payload_calls +from examples.datastore_simulator import main as main_datastore_simulator from examples.message_generator import generate_messages -from examples.message_parser import parse_messages -from examples.server_async import run_async_server +from examples.message_parser import main as main_parse_messages +from examples.server_async import setup_server from examples.server_callback import run_callback_server -from examples.server_payload import setup_payload_server -from examples.server_updating import run_updating_server, setup_updating_server -from examples.simple_async_client import run_async_client as run_simple_async_client -from examples.simple_sync_client import run_sync_client as run_simple_sync_client +from examples.server_payload import main as main_payload_server +from examples.server_sync import run_sync_server +from examples.server_updating import main as main_updating_server +from examples.simple_async_client import run_async_simple_client +from examples.simple_sync_client import run_sync_simple_client from examples.simulator import run_simulator -from pymodbus.server import ServerAsyncStop +from pymodbus.exceptions import ModbusException +from pymodbus.pdu import ExceptionResponse +from pymodbus.server import ServerAsyncStop, ServerStop BASE_PORT = 6400 @@ -42,12 +50,6 @@ class TestExamples: # awaiting fix: ("serial", "binary", BASE_PORT + 8), ] - def test_build_bcd_payload(self): - """Test build bcd payload.""" - builder = BcdPayloadBuilder() - decoder = BcdPayloadDecoder(builder) - assert str(decoder) - @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii", "binary"]) def test_message_generator(self, framer): """Test all message generator.""" @@ -55,18 +57,82 @@ def test_message_generator(self, framer): def test_message_parser(self): """Test message parser.""" - parse_messages(["--framer", "socket", "-m", "000100000006010100200001"]) - parse_messages(["--framer", "socket", "-m", "00010000000401010101"]) + main_parse_messages(["--framer", "socket", "-m", "000100000006010100200001"]) + main_parse_messages(["--framer", "socket", "-m", "00010000000401010101"]) @pytest.mark.parametrize( ("use_comm", "use_framer", "use_port"), USE_CASES, ) - async def test_client_calls(self, mock_server): + async def test_client_async_calls(self, mock_server): + """Test client_async_calls.""" + await main_client_async_calls(cmdline=mock_server) + + @pytest.mark.parametrize( + ("use_comm", "use_framer", "use_port"), + [ + ("tcp", "socket", BASE_PORT + 1), + ], + ) + async def test_client_async_calls_errors(self, mock_server): + """Test client_async_calls.""" + + async def run_template_call(client): + """Demonstrate basic read/write calls.""" + await async_template_call(client) + + client = setup_async_client(cmdline=mock_server) + client.read_coils = mock.AsyncMock(side_effect=ModbusException("test")) + with pytest.raises(ModbusException): + await run_async_client(client, modbus_calls=run_template_call) + client.read_coils = mock.AsyncMock(return_value=ExceptionResponse(0x05, 0x10)) + with pytest.raises(ModbusException): + await run_async_client(client, modbus_calls=run_template_call) + + @pytest.mark.parametrize("use_host", ["localhost"]) + @pytest.mark.parametrize( + ("use_comm", "use_framer", "use_port"), + [ + ("tcp", "socket", BASE_PORT + 1), + ("tcp", "rtu", BASE_PORT + 2), + # awaiting fix: ("tls", "tls", BASE_PORT + 3), + ("udp", "socket", BASE_PORT + 4), + ("udp", "rtu", BASE_PORT + 5), + ("serial", "rtu", BASE_PORT + 6), + # awaiting fix: ("serial", "ascii", BASE_PORT + 7), + # awaiting fix: ("serial", "binary", BASE_PORT + 8), + ], + ) + def test_client_calls(self, mock_clc, mock_cls): """Test client_calls.""" - cmdline = mock_server - test_client = setup_async_client(cmdline=cmdline) - await run_async_client(test_client, modbus_calls=run_async_calls) + server_args = setup_server(cmdline=mock_cls) + thread = Thread(target=run_sync_server, args=(server_args,)) + thread.daemon = True + thread.start() + sleep(1) + main_client_calls(cmdline=mock_clc) + ServerStop() + + @pytest.mark.parametrize( + ("use_comm", "use_framer", "use_port"), + [ + ("tcp", "socket", BASE_PORT + 1), + ], + ) + async def test_client_calls_errors(self, mock_server): + """Test client_calls.""" + + def run_template_call(client): + """Demonstrate basic read/write calls.""" + template_call(client) + + client = setup_async_client(cmdline=mock_server) + client.read_coils = mock.Mock(side_effect=ModbusException("test")) + with pytest.raises(ModbusException): + await run_async_client(client, modbus_calls=run_template_call) + client.read_coils = mock.Mock(return_value=ExceptionResponse(0x05, 0x10)) + with pytest.raises(ModbusException): + await run_async_client(client, modbus_calls=run_template_call) @pytest.mark.parametrize("use_host", ["localhost"]) @pytest.mark.parametrize( @@ -75,10 +141,10 @@ async def test_client_calls(self, mock_server): ("tcp", "socket", BASE_PORT + 41), ], ) - def xtest_custom_msg(self, use_port, mock_server): + async def test_custom_msg(self, mock_server, use_port, use_host): """Test client with custom message.""" - _cmdline = mock_server - run_custom_client("localhost", use_port) + assert mock_server + await main_custom_client(port=use_port, host=use_host) @pytest.mark.parametrize( ("use_comm", "use_framer", "use_port"), @@ -88,11 +154,9 @@ def xtest_custom_msg(self, use_port, mock_server): ) async def test_payload(self, mock_clc, mock_cls): """Test server/client with payload.""" - run_args = setup_payload_server(cmdline=mock_cls) - task = asyncio.create_task(run_async_server(run_args)) + task = asyncio.create_task(main_payload_server(cmdline=mock_cls)) await asyncio.sleep(0.1) - testclient = setup_async_client(cmdline=mock_clc) - await run_async_client(testclient, modbus_calls=run_payload_calls) + await main_payload_calls(cmdline=mock_clc) await asyncio.sleep(0.1) await ServerAsyncStop() await asyncio.sleep(0.1) @@ -103,8 +167,9 @@ async def test_payload(self, mock_clc, mock_cls): async def test_datastore_simulator(self, use_port): """Test server simulator.""" cmdargs = ["--port", str(use_port)] - run_args = setup_simulator(cmdline=cmdargs) - task = asyncio.create_task(run_server_simulator(run_args)) + task = asyncio.create_task( + main_datastore_simulator(cmdline=["--port", str(use_port)]) + ) await asyncio.sleep(0.1) cmdargs.extend(["--host", "localhost"]) testclient = setup_async_client(cmdline=cmdargs) @@ -133,38 +198,49 @@ async def test_server_callback(self, use_port): async def test_updating_server(self, use_port): """Test server simulator.""" cmdargs = ["--port", str(use_port)] - run_args = setup_updating_server(cmdline=cmdargs) - task = asyncio.create_task(run_updating_server(run_args)) + task = asyncio.create_task(main_updating_server(cmdline=cmdargs)) await asyncio.sleep(0.1) - testclient = setup_async_client(cmdline=cmdargs) - await run_async_client(testclient, modbus_calls=run_a_few_calls) + client = setup_async_client(cmdline=cmdargs) + await run_async_client(client, modbus_calls=run_a_few_calls) await asyncio.sleep(0.1) await ServerAsyncStop() await asyncio.sleep(0.1) task.cancel() await task + @pytest.mark.parametrize("use_host", ["localhost"]) @pytest.mark.parametrize( ("use_comm", "use_framer", "use_port"), [ ("tcp", "socket", BASE_PORT + 46), + # awaiting fix ("tls", "tls", BASE_PORT + 47), + ("udp", "socket", BASE_PORT + 48), + ("serial", "rtu", BASE_PORT + 49), ], ) - async def test_simple_async_client(self, use_port, mock_server, use_host): + async def test_async_simple_client(self, use_comm, use_port, mock_server, use_host): """Run simple async client.""" _cmdline = mock_server - await run_simple_async_client(use_host, use_port) + if use_comm == "serial": + use_port = f"socket://{use_host}:{use_port}" + await run_async_simple_client(use_comm, use_host, use_port) + @pytest.mark.parametrize("use_host", ["localhost"]) @pytest.mark.parametrize( ("use_comm", "use_framer", "use_port"), [ - ("tcp", "socket", BASE_PORT + 47), + ("tcp", "socket", BASE_PORT + 46), + # awaiting fix ("tls", "tls", BASE_PORT + 47), + ("udp", "socket", BASE_PORT + 48), + ("serial", "rtu", BASE_PORT + 49), ], ) - async def test_simple_sync_client(self, use_port, mock_server): + async def test_sync_simple_client(self, use_comm, use_host, use_port, mock_server): """Run simple async client.""" _cmdline = mock_server - run_simple_sync_client("127.0.0.1", str(use_port)) + if use_comm == "serial": + use_port = f"socket://{use_host}:{use_port}" + run_sync_simple_client(use_comm, use_host, use_port) async def test_simulator(self): """Run simulator server/client."""