From 29f694c8bfb928481315c61e65db2b963225ea8e Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Fri, 11 Sep 2020 14:36:14 +0530 Subject: [PATCH] #515 fix repl broadcast (#531) * 1. update requirements 2. Fix examples 3. Fix #494 - handle_local_echo 4. Fix #500 -- asyncio serial client with already running loop 5. Fix #486 - Pass serial args for asyncio serial client 6. Fix #490 - Typo in decode_data for socker_framer 7. Fix #385 - Support timeouts to break out of responspe await when server goes offline 8. Misc updates * #516 custom data block fix * Fix broadcast error with REPL client #515 * Fix #509 Wrong unit ID referenced in framers * Update documentation for serial forwarder example. Fixes #525 * Fix unit tests, support python 3.8 for tests, renamed: pymodbus/server/asyncio.py -> pymodbus/server/async_io.py and pymodbus/client/asynchronous/asyncio -> pymodbus/client/asynchronous/async_io * Ignore python3 code syntax while reporting coverage * Fix tests failing on python 3.6 and osx * Fix typo in makefile * Fix test execution errors specific to python3.6 * Osx travis issue - Fix trial 1 * Travis reverting xcode to 8.x for mac osx --- .travis.yml | 7 +- CHANGELOG.rst | 3 + Makefile | 10 +- examples/common/async_asyncio_client.py | 5 +- examples/common/asyncio_server.py | 8 +- examples/common/modbus_payload.py | 1 - .../asynchronous_asyncio_serial_client.py | 2 +- examples/contrib/serial_forwarder.py | 8 +- .../{asyncio => async_io}/__init__.py | 14 ++- .../client/asynchronous/factory/serial.py | 4 +- pymodbus/client/asynchronous/factory/tcp.py | 2 +- pymodbus/client/asynchronous/factory/tls.py | 2 +- pymodbus/client/asynchronous/factory/udp.py | 2 +- pymodbus/client/asynchronous/mixins.py | 25 ++--- .../client/asynchronous/tornado/__init__.py | 2 +- .../client/asynchronous/twisted/__init__.py | 4 +- pymodbus/repl/README.md | 53 ++++++++-- pymodbus/repl/client.py | 99 ++++++++++--------- pymodbus/repl/main.py | 9 +- pymodbus/server/{asyncio.py => async_io.py} | 11 +-- pymodbus/version.py | 2 +- requirements-tests.txt | 4 +- requirements.txt | 2 +- scripts/travis.sh | 2 +- test/test_client_async.py | 4 +- test/test_client_async_asyncio.py | 22 ++--- test/test_server_asyncio.py | 83 ++++++++-------- 27 files changed, 220 insertions(+), 170 deletions(-) rename pymodbus/client/asynchronous/{asyncio => async_io}/__init__.py (98%) rename pymodbus/server/{asyncio.py => async_io.py} (98%) diff --git a/.travis.yml b/.travis.yml index b00c82637..ae4bc08e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,22 +4,23 @@ matrix: include: - os: linux python: "2.7" - - os: linux - python: "3.5" - os: linux python: "3.6" - os: linux python: "3.7" + - os: linux + python: "3.8" - os: osx osx_image: xcode8.3 language: generic before_install: - if [ $TRAVIS_OS_NAME = osx ]; then brew update; fi - if [ $TRAVIS_OS_NAME = osx ]; then brew install openssl; fi +# - if [$TRAVIS_OS_NAME = osx ]; then python -c "import fcntl; fcntl.fcntl(1, fcntl.F_SETFL, 0)"; fi install: # - scripts/travis.sh pip install pip-accel - - scripts/travis.sh pip install -U setuptools + - if [ $TRAVIS_OS_NAME = osx ]; then scripts/travis.sh pip install -U "\"setuptools<45"\"; else pip install -U setuptools --upgrade ; fi - scripts/travis.sh pip install coveralls - scripts/travis.sh pip install --requirement=requirements-checks.txt - scripts/travis.sh pip install --requirement=requirements-tests.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ff1a47b11..37f0f5eee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,9 @@ Version 2.4.0 * Support async moduls tls server/client * Add local echo option * Add exponential backoffs on retries. +* REPL - Support broadcasts. +* Fix framers using wrong unit address. +* Update documentation for serial_forwarder example * Fix error with rtu client for `local_echo` * Fix asyncio client not working with already running loop * Fix passing serial arguments to async clients diff --git a/Makefile b/Makefile index 74003acb2..92edfa795 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ VIRTUAL_ENV ?= $(WORKON_HOME)/pymodbus PATH := $(VIRTUAL_ENV)/bin:$(PATH) MAKE := $(MAKE) --no-print-directory SHELL = bash +PYVER=$(shell python -c "import sys;t='{v[0]}.{v[1]}'.format(v=list(sys.version_info[:2]));print(t)") default: @echo 'Makefile for pymodbus' @@ -37,10 +38,16 @@ check: install @pip install --upgrade --quiet --requirement=requirements-checks.txt @flake8 + test: install @pip install --upgrade --quiet --requirement=requirements-tests.txt +ifeq ($(PYVER),3.6) + @pytest --cov=pymodbus/ --cov-report term-missing test/test_server_asyncio.py test + @coverage report --fail-under=90 -i +else @pytest --cov=pymodbus/ --cov-report term-missing - @coverage report --fail-under=90 + @coverage report --fail-under=90 -i +endif tox: install @pip install --upgrade --quiet tox && tox @@ -57,6 +64,7 @@ publish: install twine upload dist/* $(MAKE) clean + clean: @rm -Rf *.egg .eggs *.egg-info *.db .cache .coverage .tox build dist docs/build htmlcov doc/_build test/.Python test/pip-selfcheck.json test/lib/ test/include/ test/bin/ @find . -depth -type d -name __pycache__ -exec rm -Rf {} \; diff --git a/examples/common/async_asyncio_client.py b/examples/common/async_asyncio_client.py index d334850bf..7f7615953 100644 --- a/examples/common/async_asyncio_client.py +++ b/examples/common/async_asyncio_client.py @@ -16,8 +16,8 @@ # Import the required asynchronous client # ----------------------------------------------------------------------- # from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as ModbusClient - from pymodbus.client.asynchronous.udp import ( - AsyncModbusUDPClient as ModbusClient) + # from pymodbus.client.asynchronous.udp import ( + # AsyncModbusUDPClient as ModbusClient) from pymodbus.client.asynchronous import schedulers else: @@ -210,7 +210,6 @@ def run_with_no_loop(): # run_with_not_running_loop() # Run with already running loop - # run_with_already_running_loop() log.debug("") diff --git a/examples/common/asyncio_server.py b/examples/common/asyncio_server.py index 153b91c19..be34dad3d 100755 --- a/examples/common/asyncio_server.py +++ b/examples/common/asyncio_server.py @@ -12,10 +12,10 @@ # import the various server implementations # --------------------------------------------------------------------------- # import asyncio -from pymodbus.server.asyncio import StartTcpServer -from pymodbus.server.asyncio import StartTlsServer -from pymodbus.server.asyncio import StartUdpServer -from pymodbus.server.asyncio import StartSerialServer +from pymodbus.server.async_io import StartTcpServer +from pymodbus.server.async_io import StartTlsServer +from pymodbus.server.async_io import StartUdpServer +from pymodbus.server.async_io import StartSerialServer from pymodbus.device import ModbusDeviceIdentification from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSparseDataBlock diff --git a/examples/common/modbus_payload.py b/examples/common/modbus_payload.py index 5a036d771..ea31e78fe 100755 --- a/examples/common/modbus_payload.py +++ b/examples/common/modbus_payload.py @@ -174,7 +174,6 @@ def run_binary_payload_ex(): print("-" * 60) for name, value in iteritems(decoded): print("%s\t" % name, hex(value) if isinstance(value, int) else value) - # ----------------------------------------------------------------------- # # close the client diff --git a/examples/contrib/asynchronous_asyncio_serial_client.py b/examples/contrib/asynchronous_asyncio_serial_client.py index ec48cf35b..fc82d13de 100755 --- a/examples/contrib/asynchronous_asyncio_serial_client.py +++ b/examples/contrib/asynchronous_asyncio_serial_client.py @@ -2,7 +2,7 @@ if IS_PYTHON3 and PYTHON_VERSION >= (3, 4): import asyncio from serial_asyncio import create_serial_connection - from pymodbus.client.asynchronous.asyncio import ModbusClientProtocol + from pymodbus.client.asynchronous.async_io import ModbusClientProtocol from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer from pymodbus.factory import ClientDecoder else: diff --git a/examples/contrib/serial_forwarder.py b/examples/contrib/serial_forwarder.py index ece9011f9..05c71fe10 100755 --- a/examples/contrib/serial_forwarder.py +++ b/examples/contrib/serial_forwarder.py @@ -28,8 +28,14 @@ def run_serial_forwarder(): # ----------------------------------------------------------------------- # # initialize the datastore(serial client) + # Note this would send the requests on the serial client with address = 0 + # ----------------------------------------------------------------------- # - client = ModbusClient(method='rtu', port='/dev/ptyp0') + client = ModbusClient(method='rtu', port='/tmp/ptyp0') + # If required to communicate with a specified client use unit= + # in RemoteSlaveContext + # For e.g to forward the requests to slave with unit address 1 use + # store = RemoteSlaveContext(client, unit=1) store = RemoteSlaveContext(client) context = ModbusServerContext(slaves=store, single=True) diff --git a/pymodbus/client/asynchronous/asyncio/__init__.py b/pymodbus/client/asynchronous/async_io/__init__.py similarity index 98% rename from pymodbus/client/asynchronous/asyncio/__init__.py rename to pymodbus/client/asynchronous/async_io/__init__.py index 212ebead0..b4cefd486 100644 --- a/pymodbus/client/asynchronous/asyncio/__init__.py +++ b/pymodbus/client/asynchronous/async_io/__init__.py @@ -25,6 +25,16 @@ class BaseModbusAsyncClientProtocol(AsyncModbusClientMixin): factory = None transport = None + async def execute(self, request=None): + """ + Executes requests asynchronously + :param request: + :return: + """ + req = self._execute(request) + resp = await asyncio.wait_for(req, timeout=self._timeout) + return resp + def connection_made(self, transport): """ Called when a connection is made. @@ -120,9 +130,7 @@ def connected(self): def write_transport(self, packet): return self.transport.write(packet) - def _execute(self, request, **kwargs): - """ Starts the producer to send the next request to consumer.write(Frame(request)) @@ -139,7 +147,7 @@ def _dataReceived(self, data): :param data: The data returned from the server ''' _logger.debug("recv: " + " ".join([hex(byte2int(x)) for x in data])) - unit = self.framer.decode_data(data).get("uid", 0) + unit = self.framer.decode_data(data).get("unit", 0) self.framer.processIncomingPacket(data, self._handleResponse, unit=unit) def _handleResponse(self, reply, **kwargs): diff --git a/pymodbus/client/asynchronous/factory/serial.py b/pymodbus/client/asynchronous/factory/serial.py index ab5619daf..62b719c5c 100644 --- a/pymodbus/client/asynchronous/factory/serial.py +++ b/pymodbus/client/asynchronous/factory/serial.py @@ -88,8 +88,8 @@ def async_io_factory(port=None, framer=None, **kwargs): :return: asyncio event loop and serial client """ import asyncio - from pymodbus.client.asynchronous.asyncio import (ModbusClientProtocol, - AsyncioModbusSerialClient) + from pymodbus.client.asynchronous.async_io import (ModbusClientProtocol, + AsyncioModbusSerialClient) loop = kwargs.pop("loop", None) or asyncio.get_event_loop() proto_cls = kwargs.pop("proto_cls", None) or ModbusClientProtocol diff --git a/pymodbus/client/asynchronous/factory/tcp.py b/pymodbus/client/asynchronous/factory/tcp.py index fb613785d..d0155a48e 100644 --- a/pymodbus/client/asynchronous/factory/tcp.py +++ b/pymodbus/client/asynchronous/factory/tcp.py @@ -90,7 +90,7 @@ def async_io_factory(host="127.0.0.1", port=Defaults.Port, framer=None, :return: asyncio event loop and tcp client """ import asyncio - from pymodbus.client.asynchronous.asyncio import init_tcp_client + from pymodbus.client.asynchronous.async_io import init_tcp_client loop = kwargs.get("loop") or asyncio.new_event_loop() proto_cls = kwargs.get("proto_cls", None) if not loop.is_running(): diff --git a/pymodbus/client/asynchronous/factory/tls.py b/pymodbus/client/asynchronous/factory/tls.py index 0dfa81c07..3b11ebf5a 100644 --- a/pymodbus/client/asynchronous/factory/tls.py +++ b/pymodbus/client/asynchronous/factory/tls.py @@ -28,7 +28,7 @@ def async_io_factory(host="127.0.0.1", port=Defaults.TLSPort, sslctx=None, :return: asyncio event loop and tcp client """ import asyncio - from pymodbus.client.asynchronous.asyncio import init_tls_client + from pymodbus.client.asynchronous.async_io import init_tls_client loop = kwargs.get("loop") or asyncio.new_event_loop() proto_cls = kwargs.get("proto_cls", None) if not loop.is_running(): diff --git a/pymodbus/client/asynchronous/factory/udp.py b/pymodbus/client/asynchronous/factory/udp.py index 6578732e3..542ebedc3 100644 --- a/pymodbus/client/asynchronous/factory/udp.py +++ b/pymodbus/client/asynchronous/factory/udp.py @@ -65,7 +65,7 @@ def async_io_factory(host="127.0.0.1", port=Defaults.Port, framer=None, :return: asyncio event loop and udp client """ import asyncio - from pymodbus.client.asynchronous.asyncio import init_udp_client + from pymodbus.client.asynchronous.async_io import init_udp_client loop = kwargs.get("loop") or asyncio.get_event_loop() proto_cls = kwargs.get("proto_cls", None) cor = init_udp_client(proto_cls, loop, host, port) diff --git a/pymodbus/client/asynchronous/mixins.py b/pymodbus/client/asynchronous/mixins.py index 394740a2d..7af03f49a 100644 --- a/pymodbus/client/asynchronous/mixins.py +++ b/pymodbus/client/asynchronous/mixins.py @@ -1,13 +1,12 @@ import logging -import asyncio from pymodbus.client.sync import BaseModbusClient -from pymodbus.bit_read_message import * -from pymodbus.bit_write_message import * -from pymodbus.register_read_message import * -from pymodbus.register_write_message import * -from pymodbus.diag_message import * -from pymodbus.file_message import * -from pymodbus.other_message import * +# from pymodbus.bit_read_message import * +# from pymodbus.bit_write_message import * +# from pymodbus.register_read_message import * +# from pymodbus.register_write_message import * +# from pymodbus.diag_message import * +# from pymodbus.file_message import * +# from pymodbus.other_message import * from pymodbus.constants import Defaults from pymodbus.factory import ClientDecoder @@ -36,16 +35,6 @@ def __init__(self, framer=None, timeout=2, **kwargs): framer or ModbusSocketFramer(ClientDecoder()), **kwargs ) - async def execute(self, request=None): - """ - Executes requests asynchronously - :param request: - :return: - """ - req = self._execute(request) - resp = await asyncio.wait_for(req, timeout=self._timeout) - return resp - class AsyncModbusClientMixin(BaseAsyncModbusClient): """ diff --git a/pymodbus/client/asynchronous/tornado/__init__.py b/pymodbus/client/asynchronous/tornado/__init__.py index e0af49e0d..2b0cc55d6 100644 --- a/pymodbus/client/asynchronous/tornado/__init__.py +++ b/pymodbus/client/asynchronous/tornado/__init__.py @@ -75,7 +75,7 @@ def on_receive(self, *args): if not data: return LOGGER.debug("recv: " + " ".join([hex(byte2int(x)) for x in data])) - unit = self.framer.decode_data(data).get("uid", 0) + unit = self.framer.decode_data(data).get("unit", 0) self.framer.processIncomingPacket(data, self._handle_response, unit=unit) def execute(self, request=None): diff --git a/pymodbus/client/asynchronous/twisted/__init__.py b/pymodbus/client/asynchronous/twisted/__init__.py index b1841a839..e78da476f 100644 --- a/pymodbus/client/asynchronous/twisted/__init__.py +++ b/pymodbus/client/asynchronous/twisted/__init__.py @@ -40,7 +40,7 @@ def process(): from pymodbus.client.asynchronous.mixins import AsyncModbusClientMixin from pymodbus.transaction import FifoTransactionManager, DictTransactionManager from pymodbus.transaction import ModbusSocketFramer, ModbusRtuFramer -from pymodbus.compat import byte2int +from pymodbus.compat import byte2int from twisted.python.failure import Failure @@ -98,7 +98,7 @@ def dataReceived(self, data): :param data: The data returned from the server """ - unit = self.framer.decode_data(data).get("uid", 0) + unit = self.framer.decode_data(data).get("unit", 0) self.framer.processIncomingPacket(data, self._handleResponse, unit=unit) diff --git a/pymodbus/repl/README.md b/pymodbus/repl/README.md index bea1d25bc..b4f192f46 100644 --- a/pymodbus/repl/README.md +++ b/pymodbus/repl/README.md @@ -17,15 +17,16 @@ $ pip install pymodbus[repl] --upgrade ## Usage Instructions RTU and TCP are supported as of now + ``` -bash-3.2$ pymodbus.console +✗ pymodbus.console --help Usage: pymodbus.console [OPTIONS] COMMAND [ARGS]... Options: - --version Show the version and exit. - --verbose Verbose logs - --support-diag Support Diagnostic messages - --help Show this message and exit. + --version Show the version and exit. + --verbose Verbose logs + --broadcast-support Support broadcast messages + --help Show this message and exit. Commands: serial @@ -34,8 +35,9 @@ Commands: ``` TCP Options + ``` -bash-3.2$ pymodbus.console tcp --help +✗ pymodbus.console tcp --help Usage: pymodbus.console tcp [OPTIONS] Options: @@ -44,14 +46,11 @@ Options: --framer TEXT Override the default packet framer tcp|rtu --help Show this message and exit. - - - ``` SERIAL Options ``` -bash-3.2$ pymodbus.console serial --help +✗ pymodbus.console serial --help Usage: pymodbus.console serial [OPTIONS] Options: @@ -61,18 +60,24 @@ Options: --bytesize [5|6|7|8] Modbus RTU serial Number of data bits. Possible values: FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS. Defaults to 8 + --parity [N|E|O|M|S] Modbus RTU serial parity. Enable parity checking. Possible values: PARITY_NONE, PARITY_EVEN, PARITY_ODD PARITY_MARK, PARITY_SPACE. Default to 'N' + --stopbits [1|1.5|2] Modbus RTU serial stop bits. Number of stop bits. Possible values: STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO. Default to '1' + --xonxoff INTEGER Modbus RTU serial xonxoff. Enable software flow control.Defaults to 0 + --rtscts INTEGER Modbus RTU serial rtscts. Enable hardware (RTS/CTS) flow control. Defaults to 0 + --dsrdtr INTEGER Modbus RTU serial dsrdtr. Enable hardware (DSR/DTR) flow control. Defaults to 0 + --timeout FLOAT Modbus RTU serial read timeout. Defaults to 0.025 sec --write-timeout FLOAT Modbus RTU serial write timeout. Defaults to 2 sec --help Show this message and exit. @@ -275,6 +280,34 @@ null ``` +To Send broadcast requests, use `--broadcast-support` and send requests with unit id as `0`. +`write_coil`, `write_coils`, `write_register`, `write_registers` are supported. + +``` +✗ pymodbus.console --broadcast-support tcp --host 192.168.1.8 --port 5020 + +---------------------------------------------------------------------------- +__________ _____ .___ __________ .__ +\______ \___.__. / \ ____ __| _/ \______ \ ____ ______ | | + | ___< | |/ \ / \ / _ \ / __ | | _// __ \\____ \| | + | | \___ / Y ( <_> ) /_/ | | | \ ___/| |_> > |__ + |____| / ____\____|__ /\____/\____ | /\ |____|_ /\___ > __/|____/ + \/ \/ \/ \/ \/ \/|__| + v1.2.0 - [pymodbus, version 2.4.0] +---------------------------------------------------------------------------- + +> client.write_registers address=0 values=10,20,30,40 unit=0 +{ + "broadcasted": true +} + +> client.write_registers address=0 values=10,20,30,40 unit=1 +{ + "address": 0, + "count": 4 +} +``` + ## DEMO [![asciicast](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o.png)](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o) diff --git a/pymodbus/repl/client.py b/pymodbus/repl/client.py index 146779a16..c219387cb 100644 --- a/pymodbus/repl/client.py +++ b/pymodbus/repl/client.py @@ -35,29 +35,54 @@ GetClearModbusPlusRequest) +def handle_brodcast(func): + def _wrapper(*args, **kwargs): + self = args[0] + resp = func(*args, **kwargs) + if kwargs.get("unit") == 0 and self.broadcast_enable: + return { + 'broadcasted': True + } + if not resp.isError(): + return { + 'function_code': resp.function_code, + 'address': resp.address, + 'count': resp.count + } + else: + return ExtendedRequestSupport._process_exception(resp, **kwargs) + return _wrapper + + class ExtendedRequestSupport(object): @staticmethod - def _process_exception(resp): - if isinstance(resp, ExceptionResponse): + def _process_exception(resp, **kwargs): + unit = kwargs.get("unit") + if unit == 0: err = { - 'original_function_code': "{} ({})".format( - resp.original_code, hex(resp.original_code)), - 'error_function_code': "{} ({})".format( - resp.function_code, hex(resp.function_code)), - 'exception code': resp.exception_code, - 'message': ModbusExceptions.decode(resp.exception_code) - } - elif isinstance(resp, ModbusIOException): - err = { - 'original_function_code': "{} ({})".format( - resp.fcode, hex(resp.fcode)), - 'error': resp.message + "message": "Broadcast message, ignoring errors!!!" } else: - err = { - 'error': str(resp) - } + if isinstance(resp, ExceptionResponse): + err = { + 'original_function_code': "{} ({})".format( + resp.original_code, hex(resp.original_code)), + 'error_function_code': "{} ({})".format( + resp.function_code, hex(resp.function_code)), + 'exception code': resp.exception_code, + 'message': ModbusExceptions.decode(resp.exception_code) + } + elif isinstance(resp, ModbusIOException): + err = { + 'original_function_code': "{} ({})".format( + resp.fcode, hex(resp.fcode)), + 'error': resp.message + } + else: + err = { + 'error': str(resp) + } return err def read_coils(self, address, count=1, **kwargs): @@ -98,6 +123,7 @@ def read_discrete_inputs(self, address, count=1, **kwargs): else: return ExtendedRequestSupport._process_exception(resp) + @handle_brodcast def write_coil(self, address, value, **kwargs): """ Write `value` to coil at `address`. @@ -109,15 +135,9 @@ def write_coil(self, address, value, **kwargs): """ resp = super(ExtendedRequestSupport, self).write_coil( address, value, **kwargs) - if not resp.isError(): - return { - 'function_code': resp.function_code, - 'address': resp.address, - 'value': resp.value - } - else: - return ExtendedRequestSupport._process_exception(resp) + return resp + @handle_brodcast def write_coils(self, address, values, **kwargs): """ Write `value` to coil at `address`. @@ -129,15 +149,9 @@ def write_coils(self, address, values, **kwargs): """ resp = super(ExtendedRequestSupport, self).write_coils( address, values, **kwargs) - if not resp.isError(): - return { - 'function_code': resp.function_code, - 'address': resp.address, - 'count': resp.count - } - else: - return ExtendedRequestSupport._process_exception(resp) + return resp + @handle_brodcast def write_register(self, address, value, **kwargs): """ Write `value` to register at `address`. @@ -149,15 +163,9 @@ def write_register(self, address, value, **kwargs): """ resp = super(ExtendedRequestSupport, self).write_register( address, value, **kwargs) - if not resp.isError(): - return { - 'function_code': resp.function_code, - 'address': resp.address, - 'value': resp.value - } - else: - return ExtendedRequestSupport._process_exception(resp) + return resp + @handle_brodcast def write_registers(self, address, values, **kwargs): """ Write list of `values` to registers starting at `address`. @@ -169,14 +177,7 @@ def write_registers(self, address, values, **kwargs): """ resp = super(ExtendedRequestSupport, self).write_registers( address, values, **kwargs) - if not resp.isError(): - return { - 'function_code': resp.function_code, - 'address': resp.address, - 'count': resp.count - } - else: - return ExtendedRequestSupport._process_exception(resp) + return resp def read_holding_registers(self, address, count=1, **kwargs): """ diff --git a/pymodbus/repl/main.py b/pymodbus/repl/main.py index d8149368a..65e6efa77 100644 --- a/pymodbus/repl/main.py +++ b/pymodbus/repl/main.py @@ -41,7 +41,7 @@ \/ \/ \/ \/ \/ \/|__| v{} - {} ---------------------------------------------------------------------------- -""".format("1.1.0", version) +""".format("1.2.0", version) log = None @@ -226,8 +226,9 @@ def _process_args(args, string=True): @click.group('pymodbus-repl') @click.version_option(version, message=TITLE) @click.option("--verbose", is_flag=True, default=False, help="Verbose logs") +@click.option("--broadcast-support", is_flag=True, default=False, help="Support broadcast messages") @click.pass_context -def main(ctx, verbose): +def main(ctx, verbose, broadcast_support): if verbose: global log import logging @@ -236,6 +237,7 @@ def main(ctx, verbose): log = logging.getLogger('pymodbus') logging.basicConfig(format=format) log.setLevel(logging.DEBUG) + ctx.obj = {"broadcast": broadcast_support} @main.command("tcp") @@ -258,7 +260,8 @@ def main(ctx, verbose): ) def tcp(ctx, host, port, framer): from pymodbus.repl.client import ModbusTcpClient - kwargs = dict(host=host, port=port) + broadcast = ctx.obj.get("broadcast") + kwargs = dict(host=host, port=port, broadcast_enable=broadcast) if framer == 'rtu': from pymodbus.framer.rtu_framer import ModbusRtuFramer kwargs['framer'] = ModbusRtuFramer diff --git a/pymodbus/server/asyncio.py b/pymodbus/server/async_io.py similarity index 98% rename from pymodbus/server/asyncio.py rename to pymodbus/server/async_io.py index 4c05b9fb5..690332843 100755 --- a/pymodbus/server/asyncio.py +++ b/pymodbus/server/async_io.py @@ -46,7 +46,7 @@ def __init__(self, owner): self.server = owner self.running = False self.receive_queue = asyncio.Queue() - self.handler_task = None # coroutine to be run on asyncio loop + self.handler_task = None # coroutine to be run on asyncio loop def connection_made(self, transport): """ @@ -90,7 +90,6 @@ def connection_lost(self, exc): else: # pragma: no cover _logger.debug("Client Disconnection [%s:%s] due to %s" % (*self.client_address, exc)) - self.running = False except Exception as ex: # pragma: no cover @@ -126,9 +125,9 @@ async def handle(self): while self.running: try: units = self.server.context.slaves() - data = await self._recv_() # this is an asyncio.Queue await, it will never fail + data = await self._recv_() # this is an asyncio.Queue await, it will never fail if isinstance(data, tuple): - data, *addr = data # addr is populated when talking over UDP + data, *addr = data # addr is populated when talking over UDP else: addr = (None,) # empty tuple @@ -396,8 +395,8 @@ def __init__(self, if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - self.serving = self.loop.create_future() # asyncio future that will be done once server has started - self.server = None # constructors cannot be declared async, so we have to defer the initialization of the server + self.serving = self.loop.create_future() # asyncio future that will be done once server has started + self.server = None # constructors cannot be declared async, so we have to defer the initialization of the server if PYTHON_VERSION >= (3, 7): # start_serving is new in version 3.7 self.server_factory = self.loop.create_server(lambda : self.handler(self), diff --git a/pymodbus/version.py b/pymodbus/version.py index 1b71a611b..8e29cb9c9 100644 --- a/pymodbus/version.py +++ b/pymodbus/version.py @@ -41,7 +41,7 @@ def __str__(self): return '[%s, version %s]' % (self.package, self.short()) -version = Version('pymodbus', 2, 4, 0) +version = Version('pymodbus', 2, 4, 0, 'rc2') diff --git a/requirements-tests.txt b/requirements-tests.txt index 2ee2f2610..7b73548d3 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -13,7 +13,7 @@ redis>=2.10.6 sqlalchemy>=1.1.15 #wsgiref>=0.1.2 verboselogs >= 1.5 -tornado>=4.5.3 -Twisted>=17.1.0 +tornado==4.5.3 +Twisted==17.1.0 zope.interface>=4.4.0 asynctest>=0.10.0 diff --git a/requirements.txt b/requirements.txt index c44e3c1dd..b3a0eb930 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -six>=1.11.0 +six>=1.15.0 # ------------------------------------------------------------------- # if want to use the pymodbus serial stack, uncomment these # ------------------------------------------------------------------- diff --git a/scripts/travis.sh b/scripts/travis.sh index 5308eb14e..8f4338270 100755 --- a/scripts/travis.sh +++ b/scripts/travis.sh @@ -1,5 +1,5 @@ #!/bin/bash -e - +set -x if [ "$TRAVIS_OS_NAME" = osx ]; then VIRTUAL_ENV="$HOME/.virtualenvs/python2.7" if [ ! -x "$VIRTUAL_ENV/bin/python" ]; then diff --git a/test/test_client_async.py b/test/test_client_async.py index fbc12fc18..97aaae8bd 100644 --- a/test/test_client_async.py +++ b/test/test_client_async.py @@ -5,8 +5,8 @@ if IS_PYTHON3 and PYTHON_VERSION >= (3, 4): from unittest.mock import patch, Mock, MagicMock import asyncio - from pymodbus.client.asynchronous.asyncio import ReconnectingAsyncioModbusTlsClient - from pymodbus.client.asynchronous.asyncio import AsyncioModbusSerialClient + from pymodbus.client.asynchronous.async_io import ReconnectingAsyncioModbusTlsClient + from pymodbus.client.asynchronous.async_io import AsyncioModbusSerialClient from serial_asyncio import SerialTransport else: from mock import patch, Mock, MagicMock diff --git a/test/test_client_async_asyncio.py b/test/test_client_async_asyncio.py index 0bc3afc18..64c73ae27 100644 --- a/test/test_client_async_asyncio.py +++ b/test/test_client_async_asyncio.py @@ -1,9 +1,8 @@ from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION import pytest if IS_PYTHON3 and PYTHON_VERSION >= (3, 4): - import asyncio from unittest import mock - from pymodbus.client.asynchronous.asyncio import ( + from pymodbus.client.asynchronous.async_io import ( ReconnectingAsyncioModbusTcpClient, ModbusClientProtocol, ModbusUdpClientProtocol) from test.asyncio_test_helper import return_as_coroutine, run_coroutine @@ -93,7 +92,7 @@ def test_factory_protocol_made_connection(self): assert client.connected assert client.protocol is mock.sentinel.PROTOCOL - @mock.patch('pymodbus.client.asynchronous.asyncio.asyncio.ensure_future') + @mock.patch('pymodbus.client.asynchronous.async_io.asyncio.ensure_future') def test_factory_protocol_lost_connection(self, mock_async): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() @@ -109,15 +108,15 @@ def test_factory_protocol_lost_connection(self, mock_async): client.host = mock.sentinel.HOST client.port = mock.sentinel.PORT client.protocol = mock.sentinel.PROTOCOL - - with mock.patch('pymodbus.client.asynchronous.asyncio.ReconnectingAsyncioModbusTcpClient._reconnect') as mock_reconnect: + with mock.patch('pymodbus.client.asynchronous.async_io.ReconnectingAsyncioModbusTcpClient._reconnect') as mock_reconnect: mock_reconnect.return_value = mock.sentinel.RECONNECT_GENERATOR client.protocol_lost_connection(mock.sentinel.PROTOCOL) - mock_async.assert_called_once_with(mock.sentinel.RECONNECT_GENERATOR, loop=mock_loop) + if PYTHON_VERSION <= (3, 7): + mock_async.assert_called_once_with(mock.sentinel.RECONNECT_GENERATOR, loop=mock_loop) assert not client.connected assert client.protocol is None - @mock.patch('pymodbus.client.asynchronous.asyncio.asyncio.ensure_future') + @mock.patch('pymodbus.client.asynchronous.async_io.asyncio.ensure_future') def test_factory_start_success(self, mock_async): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() @@ -127,7 +126,7 @@ def test_factory_start_success(self, mock_async): mock_loop.create_connection.assert_called_once_with(mock.ANY, mock.sentinel.HOST, mock.sentinel.PORT) assert mock_async.call_count == 0 - @mock.patch('pymodbus.client.asynchronous.asyncio.asyncio.ensure_future') + @mock.patch('pymodbus.client.asynchronous.async_io.asyncio.ensure_future') def test_factory_start_failing_and_retried(self, mock_async): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() @@ -135,13 +134,14 @@ def test_factory_start_failing_and_retried(self, mock_async): client = ReconnectingAsyncioModbusTcpClient(protocol_class=mock_protocol_class, loop=mock_loop) # check whether reconnect is called upon failed connection attempt: - with mock.patch('pymodbus.client.asynchronous.asyncio.ReconnectingAsyncioModbusTcpClient._reconnect') as mock_reconnect: + with mock.patch('pymodbus.client.asynchronous.async_io.ReconnectingAsyncioModbusTcpClient._reconnect') as mock_reconnect: mock_reconnect.return_value = mock.sentinel.RECONNECT_GENERATOR run_coroutine(client.start(mock.sentinel.HOST, mock.sentinel.PORT)) mock_reconnect.assert_called_once_with() - mock_async.assert_called_once_with(mock.sentinel.RECONNECT_GENERATOR, loop=mock_loop) + if PYTHON_VERSION <= (3, 7): + mock_async.assert_called_once_with(mock.sentinel.RECONNECT_GENERATOR, loop=mock_loop) - @mock.patch('pymodbus.client.asynchronous.asyncio.asyncio.sleep') + @mock.patch('pymodbus.client.asynchronous.async_io.asyncio.sleep') def test_factory_reconnect(self, mock_sleep): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() diff --git a/test/test_server_asyncio.py b/test/test_server_asyncio.py index e7779dbb2..42cb67ad4 100755 --- a/test/test_server_asyncio.py +++ b/test/test_server_asyncio.py @@ -12,8 +12,8 @@ from pymodbus.device import ModbusDeviceIdentification from pymodbus.factory import ServerDecoder from pymodbus.server.asynchronous import ModbusTcpProtocol, ModbusUdpProtocol -from pymodbus.server.asyncio import StartTcpServer, StartTlsServer, StartUdpServer, StartSerialServer, StopServer, ModbusServerFactory -from pymodbus.server.asyncio import ModbusConnectedRequestHandler, ModbusBaseRequestHandler +from pymodbus.server.async_io import StartTcpServer, StartTlsServer, StartUdpServer, StartSerialServer, StopServer, ModbusServerFactory +from pymodbus.server.async_io import ModbusConnectedRequestHandler, ModbusBaseRequestHandler from pymodbus.datastore import ModbusSequentialDataBlock from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext from pymodbus.compat import byte2int @@ -208,7 +208,7 @@ class BasicClient(asyncio.BaseProtocol): def connection_made(self, transport): self.transport = transport step1.set_result(True) - + transport, protocol = yield from self.loop.create_connection(BasicClient, host='127.0.0.1', port=random_port) yield from step1 # await asyncio.sleep(1) @@ -253,44 +253,6 @@ def connection_made(self, transport): yield from asyncio.sleep(0.0) self.assertTrue( len(server.active_connections) == 0 ) - @asyncio.coroutine - def testTcpServerException(self): - ''' Sending garbage data on a TCP socket should drop the connection ''' - garbage = b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' - server = yield from StartTcpServer(context=self.context,address=("127.0.0.1", 0),loop=self.loop) - if PYTHON_VERSION >= (3, 7): - server_task = asyncio.create_task(server.serve_forever()) - else: - server_task = asyncio.ensure_future(server.serve_forever()) - yield from server.serving - with patch('pymodbus.transaction.ModbusSocketFramer.processIncomingPacket', - new_callable=lambda : Mock(side_effect=Exception)) as process: - connect, receive, eof = self.loop.create_future(),self.loop.create_future(),self.loop.create_future() - received_data = None - random_port = server.server.sockets[0].getsockname()[1] # get the random server port - - class BasicClient(asyncio.BaseProtocol): - def connection_made(self, transport): - _logger.debug("Client connected") - self.transport = transport - transport.write(garbage) - connect.set_result(True) - - def data_received(self, data): - _logger.debug("Client received data") - receive.set_result(True) - received_data = data - - def eof_received(self): - _logger.debug("Client stream eof") - eof.set_result(True) - - transport, protocol = yield from self.loop.create_connection(BasicClient, host='127.0.0.1',port=random_port) - yield from asyncio.wait_for(connect, timeout=0.1) - yield from asyncio.wait_for(eof, timeout=0.1) - # neither of these should timeout if the test is successful - server.server_close() - @asyncio.coroutine def testTcpServerNoSlave(self): ''' Test unknown slave unit exception ''' @@ -666,6 +628,45 @@ def testStopServer(self): with self.assertWarns(DeprecationWarning): StopServer() + @asyncio.coroutine + def testTcpServerException(self): + ''' Sending garbage data on a TCP socket should drop the connection ''' + garbage = b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' + server = yield from StartTcpServer(context=self.context, address=("127.0.0.1", 0), loop=self.loop) + if PYTHON_VERSION >= (3, 7): + server_task = asyncio.create_task(server.serve_forever()) + else: + server_task = asyncio.ensure_future(server.serve_forever()) + yield from server.serving + with patch('pymodbus.transaction.ModbusSocketFramer.processIncomingPacket', + new_callable=lambda: Mock(side_effect=Exception)) as process: + connect, receive, eof = self.loop.create_future(), self.loop.create_future(), self.loop.create_future() + received_data = None + random_port = server.server.sockets[0].getsockname()[1] # get the random server port + + class BasicClient(asyncio.BaseProtocol): + def connection_made(self, transport): + _logger.debug("Client connected") + self.transport = transport + transport.write(garbage) + connect.set_result(True) + + def data_received(self, data): + _logger.debug("Client received data") + receive.set_result(True) + received_data = data + + def eof_received(self): + _logger.debug("Client stream eof") + eof.set_result(True) + + transport, protocol = yield from self.loop.create_connection(BasicClient, host='127.0.0.1', + port=random_port) + yield from asyncio.wait_for(connect, timeout=0.1) + yield from asyncio.wait_for(eof, timeout=0.1) + # neither of these should timeout if the test is successful + server.server_close() + @asyncio.coroutine def testTcpServerException(self):