From db153e73add84bdc6fb9cdc569d0c0a06ef99dbd Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Wed, 26 Sep 2018 09:36:18 +0530 Subject: [PATCH 01/11] 1. REPL Improvements 2. Support Intercharframe timeout for Modbus RTU --- .gitignore | 2 +- pymodbus/client/sync.py | 52 ++++++---- pymodbus/exceptions.py | 7 +- pymodbus/framer/rtu_framer.py | 4 +- pymodbus/pdu.py | 6 +- pymodbus/repl/client.py | 180 +++++++++++++++++++++++++++++----- pymodbus/repl/completer.py | 4 +- pymodbus/repl/helper.py | 84 +++++++++++++--- pymodbus/repl/main.py | 26 ++--- pymodbus/transaction.py | 4 +- test/test_client_sync.py | 2 +- 11 files changed, 285 insertions(+), 86 deletions(-) diff --git a/.gitignore b/.gitignore index 9fae94a63..426321af3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,4 @@ test/__pycache__/ /doc/html/ /doc/_build/ .pytest_cache/ -/.pymodhis +**/.pymodhis diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 8bf14df03..03a315cfd 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -124,6 +124,10 @@ def __exit__(self, klass, value, traceback): self.close() def idle_time(self): + """ + Bus Idle Time to initiate next transaction + :return: time stamp + """ if self.last_frame_end is None or self.silent_interval is None: return 0 return self.last_frame_end + self.silent_interval @@ -286,11 +290,11 @@ def __repr__(self): "port={self.port}, timeout={self.timeout}>" ).format(self.__class__.__name__, hex(id(self)), self=self) - - # --------------------------------------------------------------------------- # # Modbus UDP Client Transport Implementation # --------------------------------------------------------------------------- # + + class ModbusUdpClient(BaseModbusClient): """ Implementation of a modbus udp client """ @@ -386,10 +390,14 @@ def __repr__(self): # --------------------------------------------------------------------------- # # Modbus Serial Client Transport Implementation # --------------------------------------------------------------------------- # + + class ModbusSerialClient(BaseModbusClient): """ Implementation of a modbus serial client """ state = ModbusTransactionState.IDLE + inter_char_timeout = 0 + silent_interval = 0 def __init__(self, method='ascii', **kwargs): """ Initialize a serial client instance @@ -413,18 +421,21 @@ def __init__(self, method='ascii', **kwargs): BaseModbusClient.__init__(self, self.__implementation(method, self), **kwargs) - self.port = kwargs.get('port', 0) - self.stopbits = kwargs.get('stopbits', Defaults.Stopbits) - self.bytesize = kwargs.get('bytesize', Defaults.Bytesize) - self.parity = kwargs.get('parity', Defaults.Parity) - self.baudrate = kwargs.get('baudrate', Defaults.Baudrate) - self.timeout = kwargs.get('timeout', Defaults.Timeout) - self.last_frame_end = None + self._port = kwargs.get('port', 0) + self._stopbits = kwargs.get('stopbits', Defaults.Stopbits) + self._bytesize = kwargs.get('bytesize', Defaults.Bytesize) + self._parity = kwargs.get('parity', Defaults.Parity) + self._baudrate = kwargs.get('baudrate', Defaults.Baudrate) + self._timeout = kwargs.get('timeout', Defaults.Timeout) + self.last_frame_end = 0 if self.method == "rtu": - if self.baudrate > 19200: + if self._baudrate > 19200: + self._t0 = self.inter_char_timeout = 750.0/1000000 #Micro self.silent_interval = 1.75 / 1000 # ms else: - self.silent_interval = 3.5 * (1 + 8 + 2) / self.baudrate + self._t0 = float((1 + 8 + 2)) / self._baudrate + self.inter_char_timeout = 1.5 * self._t0 + self.silent_interval = 3.5 * self._t0 self.silent_interval = round(self.silent_interval, 6) @staticmethod @@ -453,16 +464,17 @@ def connect(self): if self.socket: return True try: - self.socket = serial.Serial(port=self.port, - timeout=self.timeout, - bytesize=self.bytesize, - stopbits=self.stopbits, - baudrate=self.baudrate, - parity=self.parity) + self.socket = serial.Serial(port=self._port, + timeout=self._timeout, + bytesize=self._bytesize, + stopbits=self._stopbits, + baudrate=self._baudrate, + parity=self._parity) except serial.SerialException as msg: _logger.error(msg) self.close() if self.method == "rtu": + self.socket.interCharTimeout = self.inter_char_timeout self.last_frame_end = None return self.socket is not None @@ -512,8 +524,8 @@ def _send(self, request): return 0 def _wait_for_data(self): - if self.timeout is not None and self.timeout != 0: - condition = partial(lambda start, timeout: (time.time() - start) <= timeout, timeout=self.timeout) + if self._timeout is not None and self._timeout != 0: + condition = partial(lambda start, timeout: (time.time() - start) <= timeout, timeout=self._timeout) else: condition = partial(lambda dummy1, dummy2: True, dummy2=None) start = time.time() @@ -550,7 +562,7 @@ def __str__(self): :returns: The string representation """ - return "ModbusSerialClient(%s baud[%s])" % (self.method, self.baudrate) + return "ModbusSerialClient(%s baud[%s])" % (self.method, self._baudrate) def __repr__(self): return ( diff --git a/pymodbus/exceptions.py b/pymodbus/exceptions.py index 850804410..9c6a6844a 100644 --- a/pymodbus/exceptions.py +++ b/pymodbus/exceptions.py @@ -26,12 +26,13 @@ def isError(self): class ModbusIOException(ModbusException): ''' Error resulting from data i/o ''' - def __init__(self, string=""): + def __init__(self, string="", function_code=None): ''' Initialize the exception :param string: The message to append to the error ''' - message = "[Input/Output] %s" % string - ModbusException.__init__(self, message) + self.fcode = function_code + self.message = "[Input/Output] %s" % string + ModbusException.__init__(self, self.message) class ParameterException(ModbusException): diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py index 1a46811a1..d00edf3b3 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/rtu_framer.py @@ -243,9 +243,6 @@ def sendPacket(self, message): :param message: Message to be sent over the bus :return: """ - # _logger.debug("Current transaction state - {}".format( - # ModbusTransactionState.to_string(self.client.state)) - # ) while self.client.state != ModbusTransactionState.IDLE: if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE: ts = round(time.time(), 6) @@ -269,6 +266,7 @@ def sendPacket(self, message): else: _logger.debug("Sleeping") time.sleep(self.client.silent_interval) + self.client.state = ModbusTransactionState.IDLE size = self.client.send(message) # if size: # _logger.debug("Changing transaction state from 'SENDING' " diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index 5631ab5a0..8f5e3cea9 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -103,9 +103,9 @@ def doException(self, exception): :param exception: The exception to return :raises: An exception response """ - _logger.error("Exception Response F(%d) E(%d)" % - (self.function_code, exception)) - return ExceptionResponse(self.function_code, exception) + exc = ExceptionResponse(self.function_code, exception) + _logger.error(exc) + return exc class ModbusResponse(ModbusPDU): diff --git a/pymodbus/repl/client.py b/pymodbus/repl/client.py index 8a427e014..d60163049 100644 --- a/pymodbus/repl/client.py +++ b/pymodbus/repl/client.py @@ -4,7 +4,8 @@ """ from __future__ import absolute_import, unicode_literals -from pymodbus.pdu import ModbusExceptions +from pymodbus.pdu import ModbusExceptions, ExceptionResponse +from pymodbus.exceptions import ModbusIOException from pymodbus.client.sync import ModbusSerialClient as _ModbusSerialClient from pymodbus.client.sync import ModbusTcpClient as _ModbusTcpClient from pymodbus.mei_message import ReadDeviceInformationRequest @@ -36,15 +37,26 @@ class ExtendedRequestSupport(object): @staticmethod def _process_exception(resp): - 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) - } - return "Exception Response({})".format(err) + 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): """ @@ -81,7 +93,7 @@ def read_discrete_inputs(self, address, count=1, **kwargs): 'bits': resp.bits } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def write_coil(self, address, value, **kwargs): """ @@ -101,7 +113,7 @@ def write_coil(self, address, value, **kwargs): 'value': resp.value } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def write_coils(self, address, values, **kwargs): """ @@ -121,7 +133,7 @@ def write_coils(self, address, values, **kwargs): 'count': resp.count } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def write_register(self, address, value, **kwargs): """ @@ -140,7 +152,7 @@ def write_register(self, address, value, **kwargs): 'value': resp.value } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def write_registers(self, address, values, **kwargs): """ @@ -159,7 +171,7 @@ def write_registers(self, address, values, **kwargs): 'count': resp.count } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def read_holding_registers(self, address, count=1, **kwargs): """ @@ -177,7 +189,7 @@ def read_holding_registers(self, address, count=1, **kwargs): 'registers': resp.registers } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def read_input_registers(self, address, count=1, **kwargs): """ @@ -195,7 +207,7 @@ def read_input_registers(self, address, count=1, **kwargs): 'registers': resp.registers } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def readwrite_registers(self, read_address, read_count, write_address, write_registers, **kwargs): @@ -223,7 +235,7 @@ def readwrite_registers(self, read_address, read_count, write_address, 'registers': resp.registers } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def mask_write_register(self, address=0x0000, and_mask=0xffff, or_mask=0x0000, **kwargs): @@ -246,7 +258,7 @@ def mask_write_register(self, address=0x0000, 'or mask': resp.or_mask } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def read_device_information(self, read_code=None, object_id=0x00, **kwargs): @@ -271,7 +283,7 @@ def read_device_information(self, read_code=None, 'space left': resp.space_left } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def report_slave_id(self, **kwargs): """ @@ -289,7 +301,7 @@ def report_slave_id(self, **kwargs): 'byte count': resp.byte_count } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def read_exception_status(self, **kwargs): """ @@ -305,7 +317,7 @@ def read_exception_status(self, **kwargs): 'status': resp.status } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def get_com_event_counter(self, **kwargs): """ @@ -323,7 +335,7 @@ def get_com_event_counter(self, **kwargs): 'count': resp.count } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def get_com_event_log(self, **kwargs): """ @@ -344,7 +356,7 @@ def get_com_event_log(self, **kwargs): 'events': resp.events, } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def _execute_diagnostic_request(self, request): resp = self.execute(request) @@ -355,7 +367,7 @@ def _execute_diagnostic_request(self, request): 'message': resp.message } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) def return_query_data(self, message=0, **kwargs): """ @@ -557,6 +569,124 @@ class ModbusSerialClient(ExtendedRequestSupport, _ModbusSerialClient): def __init__(self, method, **kwargs): super(ModbusSerialClient, self).__init__(method, **kwargs) + def get_port(self): + """ + Serial Port + :return: + """ + return self._port + + def set_port(self, value): + """ + Serial Port setter + :param value: New port + :return: + """ + self._port = value + if self.is_socket_open(): + self.close() + + def get_stopbits(self): + """ + Number of stop bits. + :return: + """ + return self._stopbits + + def set_stopbits(self, value): + """ + Stop bit setter + :param value: Possible values (1, 1.5, 2) + :return: + """ + self._stopbits = float(value) + if self.is_socket_open(): + self.close() + + def get_bytesize(self): + """ + Number of data bits. + :return: + """ + return self._bytesize + + def set_bytesize(self, value): + """ + Byte size setter + :param value: Possible values (5, 6, 7, 8) + :return: + """ + self._bytesize = int(value) + if self.is_socket_open(): + self.close() + + def get_parity(self): + """ + Enable Parity Checking + :return: + """ + return self._parity + + def set_parity(self, value): + """ + Parity Setter + :param value: Possible values ('N', 'E', 'O', 'M', 'S') + :return: + """ + self._parity = value + if self.is_socket_open(): + self.close() + + def get_baudrate(self): + """ + Serial Port baudrate + :return: + """ + return self._baudrate + + def set_baudrate(self, value): + """ + Baudrate setter + :param value: + :return: + """ + self._baudrate = int(value) + if self.is_socket_open(): + self.close() + + def get_timeout(self): + """ + Serial Port Read timeout + :return: + """ + return self._timeout + + def set_timeout(self, value): + """ + Read timeout setter + :param value: Read Timeout in seconds + :return: + """ + self._timeout = float(value) + if self.is_socket_open(): + self.close() + + def get_serial_settings(self): + """ + Gets Current Serial port settings + :return: + """ + return { + 'baudrate': self._baudrate, + 'port': self._port, + 'parity': self._parity, + 'stopbits': self._stopbits, + 'bytesize': self._bytesize, + 'read timeout': self._timeout, + 't1.5': self.inter_char_timeout, + 't3.5': self.silent_interval + } + class ModbusTcpClient(ExtendedRequestSupport, _ModbusTcpClient): def __init__(self, **kwargs): diff --git a/pymodbus/repl/completer.py b/pymodbus/repl/completer.py index bfe9cbdb6..6cd4a764e 100644 --- a/pymodbus/repl/completer.py +++ b/pymodbus/repl/completer.py @@ -34,8 +34,8 @@ class CmdCompleter(Completer): :param fuzzy_match: Determines whether to use fuzzy matching. """ - def __init__(self, commands=None, ignore_case=True, **kwargs): - self._commands = commands or get_commands() + def __init__(self, client, commands=None, ignore_case=True, **kwargs): + self._commands = commands or get_commands(client) self._commands['help'] = "" self._command_names = self._commands.keys() self.ignore_case = ignore_case diff --git a/pymodbus/repl/helper.py b/pymodbus/repl/helper.py index 4ec729481..5a71c49cb 100644 --- a/pymodbus/repl/helper.py +++ b/pymodbus/repl/helper.py @@ -7,7 +7,6 @@ import pygments import inspect from collections import OrderedDict -from pymodbus.repl.client import ExtendedRequestSupport from pygments.lexers.data import JsonLexer from prompt_toolkit.formatted_text import PygmentsTokens, HTML from prompt_toolkit import print_formatted_text @@ -15,11 +14,11 @@ from pymodbus.payload import BinaryPayloadDecoder, Endian from pymodbus.compat import PYTHON_VERSION, IS_PYTHON2, string_types, izip +predicate = inspect.ismethod if IS_PYTHON2 or PYTHON_VERSION < (3, 3): - predicate = inspect.ismethod argspec = inspect.getargspec else: - predicate = inspect.isfunction + predicate = inspect.ismethod argspec = inspect.signature @@ -45,21 +44,30 @@ "result.raw": "Show RAW Result", "result.decode": "Decode register response to known formats", } +EXCLUDE = ['execute', 'recv', 'send', 'trace', 'set_debug'] +CLIENT_METHODS = [ + 'connect', 'close', 'idle_time', 'is_socket_open' +] +CLIENT_ATTRIBUTES = [] class Command(object): - def __init__(self, name, signature, doc): + def __init__(self, name, signature, doc, unit=False): self.name = name self.doc = doc.split("\n") if doc else " ".join(name.split("_")) self.help_text = self._create_help() self.param_help = self._create_arg_help() - if IS_PYTHON2: - self._params = signature + if signature: + if IS_PYTHON2: + self._params = signature + else: + self._params = signature.parameters + self.args = self.create_completion() else: - self._params = signature.parameters - self.args = self.create_completion() - if self.name.startswith("client."): + self._params = '' + + if self.name.startswith("client.") and unit: self.args.update(**DEFAULT_KWARGS) def _create_help(self): @@ -124,14 +132,53 @@ def __str__(self): return "Command {}".format(self.name) -def get_commands(): - commands = inspect.getmembers(ExtendedRequestSupport, predicate=predicate) +def _get_requests(members): + commands = list(filter(lambda x: (x[0] not in EXCLUDE + and x[0] not in CLIENT_METHODS + and callable(x[1])), + members)) + commands = { + "client.{}".format(c[0]): + Command("client.{}".format(c[0]), + argspec(c[1]), inspect.getdoc(c[1]), unit=True) + for c in commands if not c[0].startswith("_") + } + return commands + + +def _get_client_methods(members): + commands = list(filter(lambda x: (x[0] not in EXCLUDE + and x[0] in CLIENT_METHODS), + members)) commands = { "client.{}".format(c[0]): Command("client.{}".format(c[0]), - argspec(c[1]), inspect.getdoc(c[1])) + argspec(c[1]), inspect.getdoc(c[1]), unit=False) for c in commands if not c[0].startswith("_") } + return commands + + +def _get_client_properties(members): + global CLIENT_ATTRIBUTES + commands = list(filter(lambda x: not callable(x[1]), members)) + commands = { + "client.{}".format(c[0]): + Command("client.{}".format(c[0]), None, "Read Only!", unit=False) + for c in commands if (not c[0].startswith("_") + and isinstance(c[1], (string_types, int, float))) + } + CLIENT_ATTRIBUTES.extend(list(commands.keys())) + return commands + + +def get_commands(client): + commands = dict() + members = inspect.getmembers(client) + requests = _get_requests(members) + client_methods = _get_client_methods(members) + client_attr = _get_client_properties(members) + result_commands = inspect.getmembers(Result, predicate=predicate) result_commands = { "result.{}".format(c[0]): @@ -140,14 +187,23 @@ def get_commands(): for c in result_commands if (not c[0].startswith("_") and c[0] != "print_result") } + commands.update(requests) + commands.update(client_methods) + commands.update(client_attr) commands.update(result_commands) return commands class Result(object): + function_code = None + data = None + def __init__(self, result): - self.function_code = result.pop('function_code', None) - self.data = dict(result) + if isinstance(result, dict): # Modbus response + self.function_code = result.pop('function_code', None) + self.data = dict(result) + else: + self.data = result def decode(self, formatters, byte_order='big', word_order='big'): """ diff --git a/pymodbus/repl/main.py b/pymodbus/repl/main.py index eae6ceade..beb7d882e 100644 --- a/pymodbus/repl/main.py +++ b/pymodbus/repl/main.py @@ -25,7 +25,7 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from pymodbus.version import version from pymodbus.repl.completer import CmdCompleter, has_selected_completion -from pymodbus.repl.helper import Result +from pymodbus.repl.helper import Result, CLIENT_ATTRIBUTES click.disable_unicode_literals_warning = True @@ -147,7 +147,7 @@ def _process_args(args, string=True): return kwargs, execute session = PromptSession(lexer=PygmentsLexer(PythonLexer), - completer=CmdCompleter(), style=style, + completer=CmdCompleter(client), style=style, complete_while_typing=True, bottom_toolbar=bottom_toolbar, key_bindings=kb, @@ -172,17 +172,19 @@ def _process_args(args, string=True): elif text.strip().lower() == 'exit': raise EOFError() elif text.strip().lower().startswith("client."): - with client: - try: - text = text.strip().split() - cmd = text[0].split(".")[1] - args = text[1:] - kwargs, execute = _process_args(args, string=False) - if execute: + try: + text = text.strip().split() + cmd = text[0].split(".")[1] + args = text[1:] + kwargs, execute = _process_args(args, string=False) + if execute: + if text[0] in CLIENT_ATTRIBUTES: + result = Result(getattr(client, cmd)) + else: result = Result(getattr(client, cmd)(**kwargs)) - result.print_result() - except Exception as e: - click.secho(repr(e), fg='red') + result.print_result() + except Exception as e: + click.secho(repr(e), fg='red') elif text.strip().lower().startswith("result."): if result: words = text.lower().split() diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index c901de584..d869f8413 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -101,7 +101,6 @@ def _calculate_exception_length(self): return None - def execute(self, request): """ Starts the producer to send the next request to consumer.write(Frame(request)) @@ -175,7 +174,8 @@ def execute(self, request): last_exception = last_exception or ( "No Response received from the remote unit" "/Unable to decode response") - response = ModbusIOException(last_exception) + response = ModbusIOException(last_exception, + request.function_code) if hasattr(self.client, "state"): _logger.debug("Changing transaction state from " "'PROCESSING REPLY' to " diff --git a/test/test_client_sync.py b/test/test_client_sync.py index 5e5c62d7f..89bd97c95 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -349,7 +349,7 @@ def testSerialClientRepr(self): client = ModbusSerialClient() rep = "<{} at {} socket={}, method={}, timeout={}>".format( client.__class__.__name__, hex(id(client)), client.socket, - client.method, client.timeout + client.method, client._timeout ) self.assertEqual(repr(client), rep) # ---------------------------------------------------------------------------# From e73351507e0246ff725714f002adfd3eb69fe72e Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Wed, 26 Sep 2018 14:10:38 +0530 Subject: [PATCH 02/11] Fix documentation --- CHANGELOG.rst | 5 +- Makefile | 2 +- README.rst | 49 ++++++---- doc/changelog.rst | 4 + doc/conf.py | 21 +++- doc/index.rst | 2 +- doc/repl.rst | 4 - doc/source/library/REPL.md | 161 +++++++++++++++++++++++++++++++ pymodbus/framer/ascii_framer.py | 5 +- pymodbus/framer/binary_framer.py | 5 +- pymodbus/framer/rtu_framer.py | 4 +- pymodbus/framer/socket_framer.py | 10 +- pymodbus/repl/__init__.py | 2 + pymodbus/repl/client.py | 156 +++++++++++++++++------------- pymodbus/repl/completer.py | 75 ++++++++------ pymodbus/repl/helper.py | 54 ++++++++++- pymodbus/repl/main.py | 23 ++++- pymodbus/server/async.py | 21 ++-- requirements-docs.txt | 1 + 19 files changed, 445 insertions(+), 159 deletions(-) delete mode 100644 doc/repl.rst create mode 100644 doc/source/library/REPL.md diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 63f135fb0..204459f8e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,13 +31,14 @@ Version 1.5.1 * Added REPR statements for all syncchronous clients * Added `isError` method to exceptions, Any response received can be tested for success before proceeding. - ``` +.. code-block:: python + res = client.read_holding_registers(...) if not res.isError(): # proceed else: # handle error or raise - ``` + * Add examples for MEI read device information request Version 1.5.0 diff --git a/Makefile b/Makefile index 81c61b1e2..89daacc74 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ tox: install docs: install @pip install --quiet --requirement=requirements-docs.txt - @cd doc && make html + @cd doc && make clean && make html publish: install git push origin && git push --tags origin diff --git a/README.rst b/README.rst index 54b27334f..2641414ef 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,7 @@ +================================ +PyModbus - A Python Modbus Stack +================================ + .. image:: https://travis-ci.org/riptideio/pymodbus.svg?branch=master :target: https://travis-ci.org/riptideio/pymodbus .. image:: https://badges.gitter.im/Join%20Chat.svg @@ -12,9 +16,9 @@ .. important:: **Note This is a Major release and might affect your existing Async client implementation. Refer examples on how to use the latest async clients.** -============================================================ +------------------------------------------------------------ Summary -============================================================ +------------------------------------------------------------ Pymodbus is a full Modbus protocol implementation using twisted for its asynchronous communications core. It can also be used without any third @@ -23,13 +27,13 @@ needed. Furthermore, it should work fine under any python version > 2.7 (including python 3+) -============================================================ +------------------------------------------------------------ Features -============================================================ - ------------------------------------------------------------ + +~~~~~~~~~~~~~~~~~~~~ Client Features ------------------------------------------------------------- +~~~~~~~~~~~~~~~~~~~~ * Full read/write protocol on discrete and register * Most of the extended protocol (diagnostic/file/pipe/setting/information) @@ -38,9 +42,9 @@ Client Features * Payload builder/decoder utilities * Pymodbus REPL for quick tests ------------------------------------------------------------- +~~~~~~~~~~~~~~~~~~~~ Server Features ------------------------------------------------------------- +~~~~~~~~~~~~~~~~~~~~ * Can function as a fully implemented modbus server * TCP, UDP, Serial ASCII, Serial RTU, and Serial Binary @@ -48,9 +52,9 @@ Server Features * Full server control context (device information, counters, etc) * A number of backing contexts (database, redis, sqlite, a slave device) -============================================================ +------------------------------------------------------------ Use Cases -============================================================ +------------------------------------------------------------ Although most system administrators will find little need for a Modbus server on any modern hardware, they may find the need to query devices on @@ -94,9 +98,9 @@ trace them. I get a lot of email and sometimes these requests get lost in the noise: http://groups.google.com/group/pymodbus or at gitter: https://gitter.im/pymodbus_dev/Lobby -============================================================ +------------------------------------------------------------ Pymodbus REPL (Read Evaluate Procee Loop) -============================================================ +------------------------------------------------------------ Starting with Pymodbus 2.x, pymodbus library comes with handy Pymodbus REPL to quickly run the modbus clients in tcp/rtu modes. @@ -104,11 +108,14 @@ Pymodbus REPL comes with many handy features such as payload decoder to directly retrieve the values in desired format and supports all the diagnostic function codes directly . -For more info on REPL refer `Pymodbus REPL `_ +For more info on REPL refer `Pymodbus REPL `_ + +.. image:: https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o.png + :target: https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o -============================================================ +------------------------------------------------------------ Installing -============================================================ +------------------------------------------------------------ You can install using pip or easy install by issuing the following commands in a terminal window (make sure you have correct @@ -144,9 +151,9 @@ out all mentions of twisted. It should be noted that without twisted, one will only be able to run the synchronized version as the asynchronous versions uses twisted for its event loop. -============================================================ +------------------------------------------------------------ Current Work In Progress -============================================================ +------------------------------------------------------------ Since I don't have access to any live modbus devices anymore it is a bit hard to test on live hardware. However, if you would @@ -182,14 +189,14 @@ Use make to perform a range of activities make tox run the tests on all Python versions make clean cleanup all temporary files -============================================================ +------------------------------------------------------------ Contributing -============================================================ +------------------------------------------------------------ Just fork the repo and raise your PR against `dev` branch. -============================================================ +------------------------------------------------------------ License Information -============================================================ +------------------------------------------------------------ Pymodbus is built on top of code developed from/by: * Copyright (c) 2001-2005 S.W.A.C. GmbH, Germany. diff --git a/doc/changelog.rst b/doc/changelog.rst index 4d7817ae3..5f879e1e5 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1 +1,5 @@ +============ +CHANGELOGS +============ + .. include:: ../CHANGELOG.rst \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index fc92ce5fb..5eedee1f1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,6 +18,9 @@ # import os import sys +import recommonmark +from recommonmark.parser import CommonMarkParser +from recommonmark.transform import AutoStructify parent_dir = os.path.abspath(os.pardir) # examples = os.path.join(parent_dir, "examples") example_contrib = os.path.join(parent_dir, "examples/contrib") @@ -31,7 +34,7 @@ # sys.path.extend([examples, example_common, example_contrib, example_gui]) # sys.path.insert(0, os.path.abspath('../')) - +github_doc_root = 'https://github.com/riptideio/pymodbus/tree/master/doc/' # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -41,7 +44,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc'] +extensions = ['sphinx.ext.autodoc', 'recommonmark'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -49,8 +52,12 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_parsers = { + '.md': CommonMarkParser, +} + +source_suffix = ['.rst', '.md'] +# source_suffix = '.rst' # The master toctree document. master_doc = 'index' @@ -176,4 +183,10 @@ ] +def setup(app): + app.add_config_value('recommonmark_config', { + 'url_resolver': lambda url: github_doc_root + url, + 'auto_toc_tree_section': 'Contents', + }, True) + app.add_transform(AutoStructify) diff --git a/doc/index.rst b/doc/index.rst index 245e0fc6c..3ad85e03f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -12,7 +12,7 @@ Welcome to PyModbus's documentation! readme.rst changelog.rst - repl.rst + source/library/REPL source/example/modules.rst source/library/modules.rst diff --git a/doc/repl.rst b/doc/repl.rst deleted file mode 100644 index 1054eea3d..000000000 --- a/doc/repl.rst +++ /dev/null @@ -1,4 +0,0 @@ -Pymodbus REPL -============= - -.. mdinclude:: ../pymodbus/repl/README.md \ No newline at end of file diff --git a/doc/source/library/REPL.md b/doc/source/library/REPL.md new file mode 100644 index 000000000..9be3e7e09 --- /dev/null +++ b/doc/source/library/REPL.md @@ -0,0 +1,161 @@ +# Pymodbus REPL + +## Dependencies + +Depends on [prompt_toolkit](https://python-prompt-toolkit.readthedocs.io/en/stable/index.html) and [click](http://click.pocoo.org/6/quickstart/) + +Install dependencies +``` +$ pip install click prompt_toolkit --upgarde +``` + +Or +Install pymodbus with repl support +``` +$ pip install pymodbus[repl] --upgrade +``` + +## Usage Instructions +RTU and TCP are supported as of now +``` +bash-3.2$ pymodbus.console +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. + +Commands: + rtu + tcp + + +``` +TCP Options +``` +bash-3.2$ pymodbus.console tcp --help +Usage: pymodbus.console tcp [OPTIONS] + +Options: + --host TEXT Modbus TCP IP + --port INTEGER Modbus TCP port + --help Show this message and exit. + + + + +``` + +RTU Options +``` +bash-3.2$ pymodbus.console rtu --help +Usage: pymodbus.console rtu [OPTIONS] + +Options: + --method TEXT Modbus Serial Mode (rtu/ascii) + --port TEXT Modbus RTU port + --baudrate INTEGER Modbus RTU serial baudrate to use. Defaults to 9600 + --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. +``` + +To view all available commands type `help` + +``` +$ pymodbus.console tcp --host 192.168.128.126 --port 5020 + +> help +Available commands: +client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests +client.clear_counters Diagnostic sub command, Clear all counters and diag registers +client.clear_overrun_count Diagnostic sub command, Clear over run counter +client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode +client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device +client.get_com_event_counter Read status word and an event count from the remote device's communication event counter +client.get_com_event_log Read status word, event count, message count, and a field of event bytes from the remote device. +client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask` +client.read_coils Reads `count` coils from a given slave starting at `address` +client.read_device_information Read the identification and additional information of remote slave +client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address` +client.read_exception_status Read the contents of eight Exception Status outputs in a remote device. +client.read_holding_registers Read `count` number of holding registers starting at `address` +client.read_input_registers Read `count` number of input registers starting at `address` +client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address` +client.report_slave_id Report information about remote slave ID +client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters . +client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave +client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave +client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave +client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register +client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave +client.return_query_data Diagnostic sub command , Loop back data sent in response +client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition +client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave +client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave +client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave +client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave +client.write_coil Write `value` to coil at `address` +client.write_coils Write `value` to coil at `address` +client.write_register Write `value` to register at `address` +client.write_registers Write list of `values` to registers starting at `address` +result.decode Decode the register response to known formatters +result.raw Return raw result dict +``` + +Every command has auto suggetion on the arguments supported , supply arg and value are to be supplied in `arg=val` format. +``` + +> client.read_holding_registers count=4 address=9 unit=1 +{ + "registers": [ + 60497, + 47134, + 34091, + 15424 + ] +} +``` + +The last result could be accessed with `result.raw` command +``` +> result.raw +{ + "registers": [ + 15626, + 55203, + 28733, + 18368 + ] +} +``` + +For Holding and Input register reads, the decoded value could be viewed with `result.decode` +``` +> result.decode word_order=little byte_order=little formatters=float64 +28.17 + +> +``` + +## DEMO + +[![asciicast](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o.png)](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o) + diff --git a/pymodbus/framer/ascii_framer.py b/pymodbus/framer/ascii_framer.py index 9c2b1afda..4f7f99cf2 100644 --- a/pymodbus/framer/ascii_framer.py +++ b/pymodbus/framer/ascii_framer.py @@ -154,7 +154,7 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): This takes in a new request packet, adds it to the current packet stream, and performs framing on it. That is, checks for complete messages, and once found, will process all that - exist. This handles the case when we read N + 1 or 1 / N + exist. This handles the case when we read N + 1 or 1 // N messages at a time instead of 1. The processed and decoded messages are pushed to the callback @@ -162,10 +162,9 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): :param data: The new packet data :param callback: The function to send results to - :param unit: Process if unit id matches, ignore otherwise (could be a + :param unit: Process if unit id matches, ignore otherwise (could be a \ list of unit ids (server) or single unit id(client/server)) :param single: True or False (If True, ignore unit address validation) - """ if not isinstance(unit, (list, tuple)): unit = [unit] diff --git a/pymodbus/framer/binary_framer.py b/pymodbus/framer/binary_framer.py index b8602489f..9ccfbf76b 100644 --- a/pymodbus/framer/binary_framer.py +++ b/pymodbus/framer/binary_framer.py @@ -144,7 +144,7 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): This takes in a new request packet, adds it to the current packet stream, and performs framing on it. That is, checks for complete messages, and once found, will process all that - exist. This handles the case when we read N + 1 or 1 / N + exist. This handles the case when we read N + 1 or 1 // N messages at a time instead of 1. The processed and decoded messages are pushed to the callback @@ -152,10 +152,9 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): :param data: The new packet data :param callback: The function to send results to - :param unit: Process if unit id matches, ignore otherwise (could be a + :param unit: Process if unit id matches, ignore otherwise (could be a \ list of unit ids (server) or single unit id(client/server) :param single: True or False (If True, ignore unit address validation) - """ self.addToFrame(data) if not isinstance(unit, (list, tuple)): diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py index d00edf3b3..9e1aaea09 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/rtu_framer.py @@ -197,7 +197,7 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): This takes in a new request packet, adds it to the current packet stream, and performs framing on it. That is, checks for complete messages, and once found, will process all that - exist. This handles the case when we read N + 1 or 1 / N + exist. This handles the case when we read N + 1 or 1 // N messages at a time instead of 1. The processed and decoded messages are pushed to the callback @@ -205,7 +205,7 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): :param data: The new packet data :param callback: The function to send results to - :param unit: Process if unit id matches, ignore otherwise (could be a + :param unit: Process if unit id matches, ignore otherwise (could be a \ list of unit ids (server) or single unit id(client/server) :param single: True or False (If True, ignore unit address validation) """ diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/socket_framer.py index 201018960..45c8a7331 100644 --- a/pymodbus/framer/socket_framer.py +++ b/pymodbus/framer/socket_framer.py @@ -21,7 +21,7 @@ class ModbusSocketFramer(ModbusFramer): Before each modbus TCP message is an MBAP header which is used as a message frame. It allows us to easily separate messages as follows:: - [ MBAP Header ] [ Function Code] [ Data ] + [ MBAP Header ] [ Function Code] [ Data ] \ [ tid ][ pid ][ length ][ uid ] 2b 2b 2b 1b 1b Nb @@ -128,7 +128,7 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): This takes in a new request packet, adds it to the current packet stream, and performs framing on it. That is, checks for complete messages, and once found, will process all that - exist. This handles the case when we read N + 1 or 1 / N + exist. This handles the case when we read N + 1 or 1 // N messages at a time instead of 1. The processed and decoded messages are pushed to the callback @@ -136,10 +136,10 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): :param data: The new packet data :param callback: The function to send results to - :param unit: Process if unit id matches, ignore otherwise (could be a - list of unit ids (server) or single unit id(client/server) + :param unit: Process if unit id matches, ignore otherwise (could be a \ + list of unit ids (server) or single unit id(client/server) :param single: True or False (If True, ignore unit address validation) - + :return: """ if not isinstance(unit, (list, tuple)): unit = [unit] diff --git a/pymodbus/repl/__init__.py b/pymodbus/repl/__init__.py index f7bb9a10e..ff5581a33 100644 --- a/pymodbus/repl/__init__.py +++ b/pymodbus/repl/__init__.py @@ -1,4 +1,6 @@ """ +Pymodbus REPL Module. + Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved. """ diff --git a/pymodbus/repl/client.py b/pymodbus/repl/client.py index d60163049..c35795bd3 100644 --- a/pymodbus/repl/client.py +++ b/pymodbus/repl/client.py @@ -1,4 +1,6 @@ """ +Modbus Clients to be used with REPL. + Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved. """ @@ -60,7 +62,7 @@ def _process_exception(resp): def read_coils(self, address, count=1, **kwargs): """ - Reads `count` coils from a given slave starting at `address` + Reads `count` coils from a given slave starting at `address`. :param address: The starting address to read from :param count: The number of coils to read @@ -79,7 +81,8 @@ def read_coils(self, address, count=1, **kwargs): def read_discrete_inputs(self, address, count=1, **kwargs): """ - Reads `count` number of discrete inputs starting at offset `address` + Reads `count` number of discrete inputs starting at offset `address`. + :param address: The starting address to read from :param count: The number of coils to read :param unit: The slave unit this request is targeting @@ -97,7 +100,7 @@ def read_discrete_inputs(self, address, count=1, **kwargs): def write_coil(self, address, value, **kwargs): """ - Write `value` to coil at `address` + Write `value` to coil at `address`. :param address: coil offset to write to :param value: bit value to write @@ -117,7 +120,7 @@ def write_coil(self, address, value, **kwargs): def write_coils(self, address, values, **kwargs): """ - Write `value` to coil at `address` + Write `value` to coil at `address`. :param address: coil offset to write to :param value: list of bit values to write (comma seperated) @@ -137,7 +140,8 @@ def write_coils(self, address, values, **kwargs): def write_register(self, address, value, **kwargs): """ - Write `value` to register at `address` + Write `value` to register at `address`. + :param address: register offset to write to :param value: register value to write :param unit: The slave unit this request is targeting @@ -156,7 +160,8 @@ def write_register(self, address, value, **kwargs): def write_registers(self, address, values, **kwargs): """ - Write list of `values` to registers starting at `address` + Write list of `values` to registers starting at `address`. + :param address: register offset to write to :param value: list of register value to write (comma seperated) :param unit: The slave unit this request is targeting @@ -175,7 +180,8 @@ def write_registers(self, address, values, **kwargs): def read_holding_registers(self, address, count=1, **kwargs): """ - Read `count` number of holding registers starting at `address` + Read `count` number of holding registers starting at `address`. + :param address: starting register offset to read from :param count: Number of registers to read :param unit: The slave unit this request is targeting @@ -193,7 +199,8 @@ def read_holding_registers(self, address, count=1, **kwargs): def read_input_registers(self, address, count=1, **kwargs): """ - Read `count` number of input registers starting at `address` + Read `count` number of input registers starting at `address`. + :param address: starting register offset to read from to :param count: Number of registers to read :param unit: The slave unit this request is targeting @@ -212,8 +219,9 @@ def read_input_registers(self, address, count=1, **kwargs): def readwrite_registers(self, read_address, read_count, write_address, write_registers, **kwargs): """ - Read `read_count` number of holding registers starting at - `read_address` and write `write_registers` starting at `write_address` + Read `read_count` number of holding registers starting at \ + `read_address` and write `write_registers` \ + starting at `write_address`. :param read_address: register offset to read from :param read_count: Number of registers to read @@ -240,7 +248,8 @@ def readwrite_registers(self, read_address, read_count, write_address, def mask_write_register(self, address=0x0000, and_mask=0xffff, or_mask=0x0000, **kwargs): """ - Mask content of holding register at `address` with `and_mask` and `or_mask` + Mask content of holding register at `address` \ + with `and_mask` and `or_mask`. :param address: Reference address of register :param and_mask: And Mask @@ -263,7 +272,7 @@ def mask_write_register(self, address=0x0000, def read_device_information(self, read_code=None, object_id=0x00, **kwargs): """ - Read the identification and additional information of remote slave + Read the identification and additional information of remote slave. :param read_code: Read Device ID code (0x01/0x02/0x03/0x04) :param object_id: Identification of the first object to obtain. @@ -287,7 +296,8 @@ def read_device_information(self, read_code=None, def report_slave_id(self, **kwargs): """ - Report information about remote slave ID + Report information about remote slave ID. + :param unit: The slave unit this request is targeting :return: """ @@ -305,7 +315,9 @@ def report_slave_id(self, **kwargs): def read_exception_status(self, **kwargs): """ - Read the contents of eight Exception Status outputs in a remote device. + Read the contents of eight Exception Status outputs in a remote \ + device. + :param unit: The slave unit this request is targeting :return: """ @@ -321,8 +333,9 @@ def read_exception_status(self, **kwargs): def get_com_event_counter(self, **kwargs): """ - Read status word and an event count from the remote device's communication - event counter + Read status word and an event count from the remote device's \ + communication event counter. + :param unit: The slave unit this request is targeting :return: """ @@ -371,7 +384,8 @@ def _execute_diagnostic_request(self, request): def return_query_data(self, message=0, **kwargs): """ - Diagnostic sub command , Loop back data sent in response + Diagnostic sub command , Loop back data sent in response. + :param message: Message to be looped back :param unit: The slave unit this request is targeting :return: @@ -381,8 +395,9 @@ def return_query_data(self, message=0, **kwargs): def restart_comm_option(self, toggle=False, **kwargs): """ - Diagnostic sub command, initialize and restart remote devices serial + Diagnostic sub command, initialize and restart remote devices serial \ interface and clear all of its communications event counters . + :param toggle: Toggle Status [ON(0xff00)/OFF(0x0000] :param unit: The slave unit this request is targeting :return: @@ -392,7 +407,7 @@ def restart_comm_option(self, toggle=False, **kwargs): def return_diagnostic_register(self, data=0, **kwargs): """ - Diagnostic sub command, Read 16-bit diagnostic register + Diagnostic sub command, Read 16-bit diagnostic register. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -403,7 +418,7 @@ def return_diagnostic_register(self, data=0, **kwargs): def change_ascii_input_delimiter(self, data=0, **kwargs): """ - Diagnostic sub command, Change message delimiter for future requests + Diagnostic sub command, Change message delimiter for future requests. :param data: New delimiter character :param unit: The slave unit this request is targeting @@ -414,8 +429,8 @@ def change_ascii_input_delimiter(self, data=0, **kwargs): def force_listen_only_mode(self, data=0, **kwargs): """ - Diagnostic sub command, Forces the addressed remote device to - its Listen Only Mode + Diagnostic sub command, Forces the addressed remote device to \ + its Listen Only Mode. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -426,7 +441,7 @@ def force_listen_only_mode(self, data=0, **kwargs): def clear_counters(self, data=0, **kwargs): """ - Diagnostic sub command, Clear all counters and diag registers + Diagnostic sub command, Clear all counters and diag registers. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -437,8 +452,8 @@ def clear_counters(self, data=0, **kwargs): def return_bus_message_count(self, data=0, **kwargs): """ - Diagnostic sub command, Return count of message detected on bus - by remote slave + Diagnostic sub command, Return count of message detected on bus \ + by remote slave. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -449,8 +464,8 @@ def return_bus_message_count(self, data=0, **kwargs): def return_bus_com_error_count(self, data=0, **kwargs): """ - Diagnostic sub command, Return count of CRC errors - received by remote slave + Diagnostic sub command, Return count of CRC errors \ + received by remote slave. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -461,8 +476,8 @@ def return_bus_com_error_count(self, data=0, **kwargs): def return_bus_exception_error_count(self, data=0, **kwargs): """ - Diagnostic sub command, Return count of Modbus exceptions - returned by remote slave + Diagnostic sub command, Return count of Modbus exceptions \ + returned by remote slave. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -473,8 +488,8 @@ def return_bus_exception_error_count(self, data=0, **kwargs): def return_slave_message_count(self, data=0, **kwargs): """ - Diagnostic sub command, Return count of messages addressed to - remote slave + Diagnostic sub command, Return count of messages addressed to \ + remote slave. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -485,7 +500,7 @@ def return_slave_message_count(self, data=0, **kwargs): def return_slave_no_response_count(self, data=0, **kwargs): """ - Diagnostic sub command, Return count of No responses by remote slave + Diagnostic sub command, Return count of No responses by remote slave. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -496,8 +511,8 @@ def return_slave_no_response_count(self, data=0, **kwargs): def return_slave_no_ack_count(self, data=0, **kwargs): """ - Diagnostic sub command, Return count of NO ACK exceptions sent - by remote slave + Diagnostic sub command, Return count of NO ACK exceptions sent \ + by remote slave. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -508,8 +523,8 @@ def return_slave_no_ack_count(self, data=0, **kwargs): def return_slave_busy_count(self, data=0, **kwargs): """ - Diagnostic sub command, Return count of server busy exceptions sent - by remote slave + Diagnostic sub command, Return count of server busy exceptions sent \ + by remote slave. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -520,8 +535,8 @@ def return_slave_busy_count(self, data=0, **kwargs): def return_slave_bus_char_overrun_count(self, data=0, **kwargs): """ - Diagnostic sub command, Return count of messages not handled - by remote slave due to character overrun condition + Diagnostic sub command, Return count of messages not handled \ + by remote slave due to character overrun condition. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -532,8 +547,8 @@ def return_slave_bus_char_overrun_count(self, data=0, **kwargs): def return_iop_overrun_count(self, data=0, **kwargs): """ - Diagnostic sub command, Return count of iop overrun errors - by remote slave + Diagnostic sub command, Return count of iop overrun errors \ + by remote slave. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -544,7 +559,7 @@ def return_iop_overrun_count(self, data=0, **kwargs): def clear_overrun_count(self, data=0, **kwargs): """ - Diagnostic sub command, Clear over run counter + Diagnostic sub command, Clear over run counter. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -555,7 +570,8 @@ def clear_overrun_count(self, data=0, **kwargs): def get_clear_modbus_plus(self, data=0, **kwargs): """ - Diagnostic sub command, Get or clear stats of remote modbus plus device + Diagnostic sub command, Get or clear stats of remote \ + modbus plus device. :param data: Data field (0x0000) :param unit: The slave unit this request is targeting @@ -571,16 +587,17 @@ def __init__(self, method, **kwargs): def get_port(self): """ - Serial Port - :return: + Serial Port. + + :return: Current Serial port """ return self._port def set_port(self, value): """ - Serial Port setter + Serial Port setter. + :param value: New port - :return: """ self._port = value if self.is_socket_open(): @@ -589,15 +606,16 @@ def set_port(self, value): def get_stopbits(self): """ Number of stop bits. - :return: + + :return: Current Stop bits """ return self._stopbits def set_stopbits(self, value): """ - Stop bit setter + Stop bit setter. + :param value: Possible values (1, 1.5, 2) - :return: """ self._stopbits = float(value) if self.is_socket_open(): @@ -606,15 +624,17 @@ def set_stopbits(self, value): def get_bytesize(self): """ Number of data bits. - :return: + + :return: Current bytesize """ return self._bytesize def set_bytesize(self, value): """ - Byte size setter + Byte size setter. + :param value: Possible values (5, 6, 7, 8) - :return: + """ self._bytesize = int(value) if self.is_socket_open(): @@ -622,16 +642,17 @@ def set_bytesize(self, value): def get_parity(self): """ - Enable Parity Checking - :return: + Enable Parity Checking. + + :return: Current parity setting """ return self._parity def set_parity(self, value): """ - Parity Setter + Parity Setter. + :param value: Possible values ('N', 'E', 'O', 'M', 'S') - :return: """ self._parity = value if self.is_socket_open(): @@ -639,16 +660,17 @@ def set_parity(self, value): def get_baudrate(self): """ - Serial Port baudrate - :return: + Serial Port baudrate. + + :return: Current baudrate """ return self._baudrate def set_baudrate(self, value): """ - Baudrate setter + Baudrate setter. + :param value: - :return: """ self._baudrate = int(value) if self.is_socket_open(): @@ -656,16 +678,17 @@ def set_baudrate(self, value): def get_timeout(self): """ - Serial Port Read timeout - :return: + Serial Port Read timeout. + + :return: Current read imeout. """ return self._timeout def set_timeout(self, value): """ - Read timeout setter + Read timeout setter. + :param value: Read Timeout in seconds - :return: """ self._timeout = float(value) if self.is_socket_open(): @@ -673,8 +696,9 @@ def set_timeout(self, value): def get_serial_settings(self): """ - Gets Current Serial port settings - :return: + Gets Current Serial port settings. + + :return: Current Serial settings as dict. """ return { 'baudrate': self._baudrate, diff --git a/pymodbus/repl/completer.py b/pymodbus/repl/completer.py index 6cd4a764e..774fe6da5 100644 --- a/pymodbus/repl/completer.py +++ b/pymodbus/repl/completer.py @@ -1,4 +1,6 @@ """ +Command Completion for pymodbus REPL. + Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved. """ @@ -27,14 +29,17 @@ def has_selected_completion(): class CmdCompleter(Completer): - """Completer for haxor-news. - :type text_utils: :class:`utils.TextUtils` - :param text_utils: An instance of `utils.TextUtils`. - :type fuzzy_match: bool - :param fuzzy_match: Determines whether to use fuzzy matching. """ + Completer for Pymodbus REPL. + """ + + def __init__(self, client, commands=None, ignore_case=True): + """ - def __init__(self, client, commands=None, ignore_case=True, **kwargs): + :param client: Modbus Client + :param commands: Commands to be added for Completion (list) + :param ignore_case: Ignore Case while looking up for commands + """ self._commands = commands or get_commands(client) self._commands['help'] = "" self._command_names = self._commands.keys() @@ -49,14 +54,13 @@ def command_names(self): return self._commands.keys() def completing_command(self, words, word_before_cursor): - """Determine if we are currently completing the hn command. - :type words: list - :param words: The input text broken into word tokens. - :type word_before_cursor: str - :param word_before_cursor: The current word before the cursor, + """ + Determine if we are dealing with supported command. + + :param words: Input text broken in to word tokens. + :param word_before_cursor: The current word before the cursor, \ which might be one or more blank spaces. - :rtype: bool - :return: Specifies whether we are currently completing the hn command. + :return: """ if len(words) == 1 and word_before_cursor != '': return True @@ -64,13 +68,12 @@ def completing_command(self, words, word_before_cursor): return False def completing_arg(self, words, word_before_cursor): - """Determine if we are currently completing an arg. - :type words: list + """ + Determine if we are currently completing an argument. + :param words: The input text broken into word tokens. - :type word_before_cursor: str - :param word_before_cursor: The current word before the cursor, + :param word_before_cursor: The current word before the cursor, \ which might be one or more blank spaces. - :rtype: bool :return: Specifies whether we are currently completing an arg. """ if len(words) > 1 and word_before_cursor != '': @@ -78,14 +81,11 @@ def completing_arg(self, words, word_before_cursor): else: return False - def arg_completions(self, words, word_before_cursor): - """Generates arguments completions based on the input. - :type words: list + def arg_completions(self, words): + """ + Generates arguments completions based on the input. + :param words: The input text broken into word tokens. - :type word_before_cursor: str - :param word_before_cursor: The current word before the cursor, - which might be one or more blank spaces. - :rtype: list :return: A list of completions. """ cmd = words[0].strip() @@ -99,18 +99,25 @@ def _get_completions(self, word, word_before_cursor): return self.word_matches(word, word_before_cursor) def word_matches(self, word, word_before_cursor): - """ True when the word before the cursor matches. """ + """ + Match the word and word before cursor + + :param words: The input text broken into word tokens. + :param word_before_cursor: The current word before the cursor, \ + which might be one or more blank spaces. + :return: True if matched. + + """ if self.ignore_case: word = word.lower() return word.startswith(word_before_cursor) def get_completions(self, document, complete_event): - """Get completions for the current scope. - :type document: :class:`prompt_toolkit.Document` + """ + Get completions for the current scope. + :param document: An instance of `prompt_toolkit.Document`. - :type _: :class:`prompt_toolkit.completion.Completion` - :param _: (Unused). - :rtype: generator + :param complete_event: (Unused). :return: Yields an instance of `prompt_toolkit.completion.Completion`. """ word_before_cursor = document.get_word_before_cursor(WORD=True) @@ -123,7 +130,11 @@ def get_completions(self, document, complete_event): pass if self.completing_command(words, word_before_cursor): commands = self._command_names - c_meta = {k: v.help_text if not isinstance(v, string_types) else v for k, v in self._commands.items()} + c_meta = { + k: v.help_text + if not isinstance(v, string_types) + else v for k, v in self._commands.items() + } meta = lambda x: (x, c_meta.get(x, '')) else: if not list(filter(lambda cmd: any(x == cmd for x in words), diff --git a/pymodbus/repl/helper.py b/pymodbus/repl/helper.py index 5a71c49cb..ba94fd686 100644 --- a/pymodbus/repl/helper.py +++ b/pymodbus/repl/helper.py @@ -1,4 +1,6 @@ """ +Helper Module for REPL actions. + Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved. """ @@ -52,8 +54,17 @@ class Command(object): - + """ + Class representing Commands to be consumed by Completer. + """ def __init__(self, name, signature, doc, unit=False): + """ + + :param name: Name of the command + :param signature: inspect object + :param doc: Doc string for the command + :param unit: Use unit as additional argument in the command . + """ self.name = name self.doc = doc.split("\n") if doc else " ".join(name.split("_")) self.help_text = self._create_help() @@ -87,6 +98,11 @@ def _create_arg_help(self): return param_dict def create_completion(self): + """ + Create command completion meta data. + + :return: + """ words = {} def _create(entry, default): @@ -119,9 +135,20 @@ def _create(entry, default): return words def get_completion(self): + """ + Gets a list of completions. + + :return: + """ return self.args.keys() def get_meta(self, cmd): + """ + Get Meta info of a given command. + + :param cmd: Name of command. + :return: Dict containing meta info. + """ cmd = cmd.strip() cmd = cmd.split("=")[0].strip() return cmd, self.param_help.get(cmd, '') @@ -173,6 +200,13 @@ def _get_client_properties(members): def get_commands(client): + """ + Helper method to retrieve all required methods and attributes of a client \ + object and convert it to commands. + + :param client: Modbus Client object. + :return: + """ commands = dict() members = inspect.getmembers(client) requests = _get_requests(members) @@ -195,10 +229,16 @@ def get_commands(client): class Result(object): + """ + Represent result command. + """ function_code = None data = None def __init__(self, result): + """ + :param result: Response of a modbus command. + """ if isinstance(result, dict): # Modbus response self.function_code = result.pop('function_code', None) self.data = dict(result) @@ -207,7 +247,8 @@ def __init__(self, result): def decode(self, formatters, byte_order='big', word_order='big'): """ - Decode the register response to known formatters + Decode the register response to known formatters. + :param formatters: int8/16/32/64, uint8/16/32/64, float32/64 :param byte_order: little/big :param word_order: little/big @@ -242,7 +283,8 @@ def decode(self, formatters, byte_order='big', word_order='big'): def raw(self): """ - Return raw result dict + Return raw result dict. + :return: """ self.print_result() @@ -261,6 +303,12 @@ def _process_dict(self, d): return new_dict def print_result(self, data=None): + """ + Prettu print result object. + + :param data: Data to be printed. + :return: + """ data = data or self.data if isinstance(data, dict): data = self._process_dict(data) diff --git a/pymodbus/repl/main.py b/pymodbus/repl/main.py index beb7d882e..b2821f64a 100644 --- a/pymodbus/repl/main.py +++ b/pymodbus/repl/main.py @@ -1,4 +1,6 @@ """ +Pymodbus REPL Entry point. + Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved. """ @@ -52,12 +54,23 @@ def bottom_toolbar(): + """ + Console toolbar. + :return: + """ return HTML('Press ' ' to exit! Type "help" for list of available commands') class CaseInsenstiveChoice(click.Choice): + """ + Case Insensitive choice for click commands and options + """ def convert(self, value, param, ctx): + """ + Convert args to uppercase for evaluation. + + """ if value is None: return None return super(CaseInsenstiveChoice, self).convert( @@ -65,6 +78,9 @@ def convert(self, value, param, ctx): class NumericChoice(click.Choice): + """ + Numeric choice for click arguments and options. + """ def __init__(self, choices, typ): self.typ = typ super(NumericChoice, self).__init__(choices) @@ -85,11 +101,13 @@ def convert(self, value, param, ctx): def cli(client): - kb = KeyBindings() + @kb.add('c-space') def _(event): - """Initialize autocompletion, or select the next completion. """ + """ + Initialize autocompletion, or select the next completion. + """ buff = event.app.current_buffer if buff.complete_state: buff.complete_next() @@ -237,7 +255,6 @@ def tcp(ctx, host, port): cli(client) - @main.command("rtu") @click.pass_context @click.option( diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 8946ed4ee..d58027a0d 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -231,15 +231,16 @@ def _is_main_thread(): def StartTcpServer(context, identity=None, address=None, console=False, defer_reactor_run=False, **kwargs): - """ Helper method to start the Modbus Async TCP server + """ + Helper method to start the Modbus Async TCP server :param context: The server data context :param identify: The server identity to use (default empty) :param address: An optional (interface, port) to bind to. :param console: A flag indicating if you want the debug console - :param ignore_missing_slaves: True to not send errors on a request + :param ignore_missing_slaves: True to not send errors on a request \ to a missing slave - :param defer_reactor_run: True/False defer running reactor.run() as part + :param defer_reactor_run: True/False defer running reactor.run() as part \ of starting server, to be explictly started by the user """ from twisted.internet import reactor @@ -258,14 +259,15 @@ def StartTcpServer(context, identity=None, address=None, def StartUdpServer(context, identity=None, address=None, defer_reactor_run=False, **kwargs): - """ Helper method to start the Modbus Async Udp server + """ + Helper method to start the Modbus Async Udp server :param context: The server data context :param identify: The server identity to use (default empty) :param address: An optional (interface, port) to bind to. - :param ignore_missing_slaves: True to not send errors on a request + :param ignore_missing_slaves: True to not send errors on a request \ to a missing slave - :param defer_reactor_run: True/False defer running reactor.run() as part + :param defer_reactor_run: True/False defer running reactor.run() as part \ of starting server, to be explictly started by the user """ from twisted.internet import reactor @@ -284,7 +286,8 @@ def StartSerialServer(context, identity=None, framer=ModbusAsciiFramer, defer_reactor_run=False, **kwargs): - """ Helper method to start the Modbus Async Serial server + """ + Helper method to start the Modbus Async Serial server :param context: The server data context :param identify: The server identity to use (default empty) @@ -292,9 +295,9 @@ def StartSerialServer(context, identity=None, :param port: The serial port to attach to :param baudrate: The baud rate to use for the serial device :param console: A flag indicating if you want the debug console - :param ignore_missing_slaves: True to not send errors on a request to a + :param ignore_missing_slaves: True to not send errors on a request to a \ missing slave - :param defer_reactor_run: True/False defer running reactor.run() as part + :param defer_reactor_run: True/False defer running reactor.run() as part \ of starting server, to be explictly started by the user """ from twisted.internet import reactor diff --git a/requirements-docs.txt b/requirements-docs.txt index ee761157b..5459f6e89 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -5,6 +5,7 @@ pyasn1==0.4.2 # Required to parse some files pyserial-asyncio==0.4.0;python_version>="3.4" pyserial==3.4 # Required to parse some files redis==2.10.6 # Required to parse some files +recommonmark==0.4.0 Sphinx==1.6.5 sphinx-rtd-theme==0.2.4 SQLAlchemy==1.1.15 # Required to parse some files From 2ef0e5de59dd362b519826ba122b9fd6e7f559f5 Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Wed, 26 Sep 2018 15:08:14 +0530 Subject: [PATCH 03/11] Revert changes to sync client and test --- pymodbus/client/sync.py | 37 ++++++++++++++++++------------------- test/test_client_sync.py | 2 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 03a315cfd..2f1da7270 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -421,19 +421,18 @@ def __init__(self, method='ascii', **kwargs): BaseModbusClient.__init__(self, self.__implementation(method, self), **kwargs) - self._port = kwargs.get('port', 0) - self._stopbits = kwargs.get('stopbits', Defaults.Stopbits) - self._bytesize = kwargs.get('bytesize', Defaults.Bytesize) - self._parity = kwargs.get('parity', Defaults.Parity) - self._baudrate = kwargs.get('baudrate', Defaults.Baudrate) - self._timeout = kwargs.get('timeout', Defaults.Timeout) - self.last_frame_end = 0 + self.port = kwargs.get('port', 0) + self.stopbits = kwargs.get('stopbits', Defaults.Stopbits) + self.bytesize = kwargs.get('bytesize', Defaults.Bytesize) + self.parity = kwargs.get('parity', Defaults.Parity) + self.baudrate = kwargs.get('baudrate', Defaults.Baudrate) + self.timeout = kwargs.get('timeout', Defaults.Timeout) + self.last_frame_end = None if self.method == "rtu": - if self._baudrate > 19200: - self._t0 = self.inter_char_timeout = 750.0/1000000 #Micro + if self.baudrate > 19200: self.silent_interval = 1.75 / 1000 # ms else: - self._t0 = float((1 + 8 + 2)) / self._baudrate + self._t0 = float((1 + 8 + 2)) / self.baudrate self.inter_char_timeout = 1.5 * self._t0 self.silent_interval = 3.5 * self._t0 self.silent_interval = round(self.silent_interval, 6) @@ -464,12 +463,12 @@ def connect(self): if self.socket: return True try: - self.socket = serial.Serial(port=self._port, - timeout=self._timeout, - bytesize=self._bytesize, - stopbits=self._stopbits, - baudrate=self._baudrate, - parity=self._parity) + self.socket = serial.Serial(port=self.port, + timeout=self.timeout, + bytesize=self.bytesize, + stopbits=self.stopbits, + baudrate=self.baudrate, + parity=self.parity) except serial.SerialException as msg: _logger.error(msg) self.close() @@ -524,8 +523,8 @@ def _send(self, request): return 0 def _wait_for_data(self): - if self._timeout is not None and self._timeout != 0: - condition = partial(lambda start, timeout: (time.time() - start) <= timeout, timeout=self._timeout) + if self.timeout is not None and self.timeout != 0: + condition = partial(lambda start, timeout: (time.time() - start) <= timeout, timeout=self.timeout) else: condition = partial(lambda dummy1, dummy2: True, dummy2=None) start = time.time() @@ -562,7 +561,7 @@ def __str__(self): :returns: The string representation """ - return "ModbusSerialClient(%s baud[%s])" % (self.method, self._baudrate) + return "ModbusSerialClient(%s baud[%s])" % (self.method, self.baudrate) def __repr__(self): return ( diff --git a/test/test_client_sync.py b/test/test_client_sync.py index 89bd97c95..5e5c62d7f 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -349,7 +349,7 @@ def testSerialClientRepr(self): client = ModbusSerialClient() rep = "<{} at {} socket={}, method={}, timeout={}>".format( client.__class__.__name__, hex(id(client)), client.socket, - client.method, client._timeout + client.method, client.timeout ) self.assertEqual(repr(client), rep) # ---------------------------------------------------------------------------# From b63e96fa031a361e4ae1a0c3fb224bf2467c5307 Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Wed, 26 Sep 2018 15:08:53 +0530 Subject: [PATCH 04/11] Rename repl command `rtu` to `serial` . Update README --- doc/source/library/REPL.md | 117 +++++++++++++++++++++++++------------ pymodbus/repl/README.md | 115 ++++++++++++++++++++++++------------ pymodbus/repl/client.py | 36 ++++++------ pymodbus/repl/completer.py | 4 +- pymodbus/repl/helper.py | 6 +- pymodbus/repl/main.py | 6 +- 6 files changed, 188 insertions(+), 96 deletions(-) diff --git a/doc/source/library/REPL.md b/doc/source/library/REPL.md index 9be3e7e09..7d6e77305 100644 --- a/doc/source/library/REPL.md +++ b/doc/source/library/REPL.md @@ -28,7 +28,7 @@ Options: --help Show this message and exit. Commands: - rtu + serial tcp @@ -50,8 +50,8 @@ Options: RTU Options ``` -bash-3.2$ pymodbus.console rtu --help -Usage: pymodbus.console rtu [OPTIONS] +bash-3.2$ pymodbus.console serial --help +Usage: pymodbus.console serial [OPTIONS] Options: --method TEXT Modbus Serial Mode (rtu/ascii) @@ -84,40 +84,49 @@ $ pymodbus.console tcp --host 192.168.128.126 --port 5020 > help Available commands: -client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests -client.clear_counters Diagnostic sub command, Clear all counters and diag registers -client.clear_overrun_count Diagnostic sub command, Clear over run counter -client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode -client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device -client.get_com_event_counter Read status word and an event count from the remote device's communication event counter +client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests. +client.clear_counters Diagnostic sub command, Clear all counters and diag registers. +client.clear_overrun_count Diagnostic sub command, Clear over run counter. +client.close Closes the underlying socket connection +client.connect Connect to the modbus tcp server +client.debug_enabled Returns a boolean indicating if debug is enabled. +client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode. +client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device. +client.get_com_event_counter Read status word and an event count from the remote device's communication event counter. client.get_com_event_log Read status word, event count, message count, and a field of event bytes from the remote device. -client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask` -client.read_coils Reads `count` coils from a given slave starting at `address` -client.read_device_information Read the identification and additional information of remote slave -client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address` -client.read_exception_status Read the contents of eight Exception Status outputs in a remote device. -client.read_holding_registers Read `count` number of holding registers starting at `address` -client.read_input_registers Read `count` number of input registers starting at `address` -client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address` -client.report_slave_id Report information about remote slave ID -client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters . -client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave -client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave -client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave -client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register -client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave -client.return_query_data Diagnostic sub command , Loop back data sent in response -client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition -client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave -client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave -client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave -client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave -client.write_coil Write `value` to coil at `address` -client.write_coils Write `value` to coil at `address` -client.write_register Write `value` to register at `address` -client.write_registers Write list of `values` to registers starting at `address` -result.decode Decode the register response to known formatters -result.raw Return raw result dict +client.host Read Only! +client.idle_time Bus Idle Time to initiate next transaction +client.is_socket_open Check whether the underlying socket/serial is open or not. +client.last_frame_end Read Only! +client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask`. +client.port Read Only! +client.read_coils Reads `count` coils from a given slave starting at `address`. +client.read_device_information Read the identification and additional information of remote slave. +client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address`. +client.read_exception_status Read the contents of eight Exception Status outputs in a remote device. +client.read_holding_registers Read `count` number of holding registers starting at `address`. +client.read_input_registers Read `count` number of input registers starting at `address`. +client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address`. +client.report_slave_id Report information about remote slave ID. +client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters . +client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave. +client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave. +client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave. +client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register. +client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave. +client.return_query_data Diagnostic sub command , Loop back data sent in response. +client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition. +client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave. +client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave. +client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave. +client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave. +client.silent_interval Read Only! +client.state Read Only! +client.timeout Read Only! +client.write_coil Write `value` to coil at `address`. +client.write_coils Write `value` to coil at `address`. +client.write_register Write `value` to register at `address`. +client.write_registers Write list of `values` to registers starting at `address`. ``` Every command has auto suggetion on the arguments supported , supply arg and value are to be supplied in `arg=val` format. @@ -155,7 +164,41 @@ For Holding and Input register reads, the decoded value could be viewed with `re > ``` -## DEMO +Client settings could be retrived and altered as well. +``` +> # For serial settings +> client.get_serial_settings +{ + "t1.5": 0.00171875, + "baudrate": 9600, + "read timeout": 0.5, + "port": "/dev/ptyp0", + "t3.5": 0.00401, + "bytesize": 8, + "parity": "N", + "stopbits": 1.0 +} +> client.set_timeout value=1 +null + +> client.get_timeout +1.0 + +> client.get_serial_settings +{ + "t1.5": 0.00171875, + "baudrate": 9600, + "read timeout": 1.0, + "port": "/dev/ptyp0", + "t3.5": 0.00401, + "bytesize": 8, + "parity": "N", + "stopbits": 1.0 +} + +``` + +#DEMO [![asciicast](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o.png)](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o) diff --git a/pymodbus/repl/README.md b/pymodbus/repl/README.md index 7097dcdcd..7d6e77305 100644 --- a/pymodbus/repl/README.md +++ b/pymodbus/repl/README.md @@ -28,7 +28,7 @@ Options: --help Show this message and exit. Commands: - rtu + serial tcp @@ -50,8 +50,8 @@ Options: RTU Options ``` -bash-3.2$ pymodbus.console rtu --help -Usage: pymodbus.console rtu [OPTIONS] +bash-3.2$ pymodbus.console serial --help +Usage: pymodbus.console serial [OPTIONS] Options: --method TEXT Modbus Serial Mode (rtu/ascii) @@ -84,40 +84,49 @@ $ pymodbus.console tcp --host 192.168.128.126 --port 5020 > help Available commands: -client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests -client.clear_counters Diagnostic sub command, Clear all counters and diag registers -client.clear_overrun_count Diagnostic sub command, Clear over run counter -client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode -client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device -client.get_com_event_counter Read status word and an event count from the remote device's communication event counter +client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests. +client.clear_counters Diagnostic sub command, Clear all counters and diag registers. +client.clear_overrun_count Diagnostic sub command, Clear over run counter. +client.close Closes the underlying socket connection +client.connect Connect to the modbus tcp server +client.debug_enabled Returns a boolean indicating if debug is enabled. +client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode. +client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device. +client.get_com_event_counter Read status word and an event count from the remote device's communication event counter. client.get_com_event_log Read status word, event count, message count, and a field of event bytes from the remote device. -client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask` -client.read_coils Reads `count` coils from a given slave starting at `address` -client.read_device_information Read the identification and additional information of remote slave -client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address` -client.read_exception_status Read the contents of eight Exception Status outputs in a remote device. -client.read_holding_registers Read `count` number of holding registers starting at `address` -client.read_input_registers Read `count` number of input registers starting at `address` -client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address` -client.report_slave_id Report information about remote slave ID -client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters . -client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave -client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave -client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave -client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register -client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave -client.return_query_data Diagnostic sub command , Loop back data sent in response -client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition -client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave -client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave -client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave -client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave -client.write_coil Write `value` to coil at `address` -client.write_coils Write `value` to coil at `address` -client.write_register Write `value` to register at `address` -client.write_registers Write list of `values` to registers starting at `address` -result.decode Decode the register response to known formatters -result.raw Return raw result dict +client.host Read Only! +client.idle_time Bus Idle Time to initiate next transaction +client.is_socket_open Check whether the underlying socket/serial is open or not. +client.last_frame_end Read Only! +client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask`. +client.port Read Only! +client.read_coils Reads `count` coils from a given slave starting at `address`. +client.read_device_information Read the identification and additional information of remote slave. +client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address`. +client.read_exception_status Read the contents of eight Exception Status outputs in a remote device. +client.read_holding_registers Read `count` number of holding registers starting at `address`. +client.read_input_registers Read `count` number of input registers starting at `address`. +client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address`. +client.report_slave_id Report information about remote slave ID. +client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters . +client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave. +client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave. +client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave. +client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register. +client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave. +client.return_query_data Diagnostic sub command , Loop back data sent in response. +client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition. +client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave. +client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave. +client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave. +client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave. +client.silent_interval Read Only! +client.state Read Only! +client.timeout Read Only! +client.write_coil Write `value` to coil at `address`. +client.write_coils Write `value` to coil at `address`. +client.write_register Write `value` to register at `address`. +client.write_registers Write list of `values` to registers starting at `address`. ``` Every command has auto suggetion on the arguments supported , supply arg and value are to be supplied in `arg=val` format. @@ -155,6 +164,40 @@ For Holding and Input register reads, the decoded value could be viewed with `re > ``` +Client settings could be retrived and altered as well. +``` +> # For serial settings +> client.get_serial_settings +{ + "t1.5": 0.00171875, + "baudrate": 9600, + "read timeout": 0.5, + "port": "/dev/ptyp0", + "t3.5": 0.00401, + "bytesize": 8, + "parity": "N", + "stopbits": 1.0 +} +> client.set_timeout value=1 +null + +> client.get_timeout +1.0 + +> client.get_serial_settings +{ + "t1.5": 0.00171875, + "baudrate": 9600, + "read timeout": 1.0, + "port": "/dev/ptyp0", + "t3.5": 0.00401, + "bytesize": 8, + "parity": "N", + "stopbits": 1.0 +} + +``` + #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 c35795bd3..665ba2b34 100644 --- a/pymodbus/repl/client.py +++ b/pymodbus/repl/client.py @@ -591,7 +591,7 @@ def get_port(self): :return: Current Serial port """ - return self._port + return self.port def set_port(self, value): """ @@ -599,7 +599,7 @@ def set_port(self, value): :param value: New port """ - self._port = value + self.port = value if self.is_socket_open(): self.close() @@ -609,7 +609,7 @@ def get_stopbits(self): :return: Current Stop bits """ - return self._stopbits + return self.stopbits def set_stopbits(self, value): """ @@ -617,7 +617,7 @@ def set_stopbits(self, value): :param value: Possible values (1, 1.5, 2) """ - self._stopbits = float(value) + self.stopbits = float(value) if self.is_socket_open(): self.close() @@ -627,7 +627,7 @@ def get_bytesize(self): :return: Current bytesize """ - return self._bytesize + return self.bytesize def set_bytesize(self, value): """ @@ -636,7 +636,7 @@ def set_bytesize(self, value): :param value: Possible values (5, 6, 7, 8) """ - self._bytesize = int(value) + self.bytesize = int(value) if self.is_socket_open(): self.close() @@ -646,7 +646,7 @@ def get_parity(self): :return: Current parity setting """ - return self._parity + return self.parity def set_parity(self, value): """ @@ -654,7 +654,7 @@ def set_parity(self, value): :param value: Possible values ('N', 'E', 'O', 'M', 'S') """ - self._parity = value + self.parity = value if self.is_socket_open(): self.close() @@ -664,7 +664,7 @@ def get_baudrate(self): :return: Current baudrate """ - return self._baudrate + return self.baudrate def set_baudrate(self, value): """ @@ -672,7 +672,7 @@ def set_baudrate(self, value): :param value: """ - self._baudrate = int(value) + self.baudrate = int(value) if self.is_socket_open(): self.close() @@ -682,7 +682,7 @@ def get_timeout(self): :return: Current read imeout. """ - return self._timeout + return self.timeout def set_timeout(self, value): """ @@ -690,7 +690,7 @@ def set_timeout(self, value): :param value: Read Timeout in seconds """ - self._timeout = float(value) + self.timeout = float(value) if self.is_socket_open(): self.close() @@ -701,12 +701,12 @@ def get_serial_settings(self): :return: Current Serial settings as dict. """ return { - 'baudrate': self._baudrate, - 'port': self._port, - 'parity': self._parity, - 'stopbits': self._stopbits, - 'bytesize': self._bytesize, - 'read timeout': self._timeout, + 'baudrate': self.baudrate, + 'port': self.port, + 'parity': self.parity, + 'stopbits': self.stopbits, + 'bytesize': self.bytesize, + 'read timeout': self.timeout, 't1.5': self.inter_char_timeout, 't3.5': self.silent_interval } diff --git a/pymodbus/repl/completer.py b/pymodbus/repl/completer.py index 774fe6da5..391c245a1 100644 --- a/pymodbus/repl/completer.py +++ b/pymodbus/repl/completer.py @@ -81,11 +81,13 @@ def completing_arg(self, words, word_before_cursor): else: return False - def arg_completions(self, words): + def arg_completions(self, words, word_before_cursor): """ Generates arguments completions based on the input. :param words: The input text broken into word tokens. + :param word_before_cursor: The current word before the cursor, \ + which might be one or more blank spaces. :return: A list of completions. """ cmd = words[0].strip() diff --git a/pymodbus/repl/helper.py b/pymodbus/repl/helper.py index ba94fd686..eff1023c7 100644 --- a/pymodbus/repl/helper.py +++ b/pymodbus/repl/helper.py @@ -48,7 +48,11 @@ } EXCLUDE = ['execute', 'recv', 'send', 'trace', 'set_debug'] CLIENT_METHODS = [ - 'connect', 'close', 'idle_time', 'is_socket_open' + 'connect', 'close', 'idle_time', 'is_socket_open', 'get_port', 'set_port', + 'get_stopbits', 'set_stopbits', 'get_bytesize', 'set_bytesize', + 'get_parity', 'set_parity', 'get_baudrate', 'set_baudrate', 'get_timeout', + 'set_timeout', 'get_serial_settings' + ] CLIENT_ATTRIBUTES = [] diff --git a/pymodbus/repl/main.py b/pymodbus/repl/main.py index b2821f64a..0d8e72885 100644 --- a/pymodbus/repl/main.py +++ b/pymodbus/repl/main.py @@ -255,7 +255,7 @@ def tcp(ctx, host, port): cli(client) -@main.command("rtu") +@main.command("serial") @click.pass_context @click.option( "--method", @@ -333,8 +333,8 @@ def tcp(ctx, host, port): default=2, type=float ) -def rtu(ctx, method, port, baudrate, bytesize, parity, stopbits, xonxoff, - rtscts, dsrdtr, timeout, write_timeout): +def serial(ctx, method, port, baudrate, bytesize, parity, stopbits, xonxoff, + rtscts, dsrdtr, timeout, write_timeout): from pymodbus.repl.client import ModbusSerialClient client = ModbusSerialClient(method=method, port=port, From 07f3eb31ade2e5f95e77d58caff52faf3365e888 Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Wed, 26 Sep 2018 15:30:15 +0530 Subject: [PATCH 05/11] Fix unicode decode error with report slave id request --- pymodbus/repl/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/repl/client.py b/pymodbus/repl/client.py index 665ba2b34..33602606c 100644 --- a/pymodbus/repl/client.py +++ b/pymodbus/repl/client.py @@ -306,7 +306,7 @@ def report_slave_id(self, **kwargs): if not resp.isError(): return { 'function_code': resp.function_code, - 'identifier': resp.identifier, + 'identifier': resp.identifier.decode('cp1252'), 'status': resp.status, 'byte count': resp.byte_count } From 97227ab483c1fa1a179866ccacb8ab2dfb6541ab Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Mon, 1 Oct 2018 18:10:53 +0530 Subject: [PATCH 06/11] #333 Fix infinite sleep loop --- pymodbus/framer/rtu_framer.py | 16 ++++++++++------ pymodbus/version.py | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py index 1a46811a1..b54a17943 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/rtu_framer.py @@ -92,7 +92,7 @@ def checkFrame(self): crc = self._buffer[frame_size - 2:frame_size] crc_val = (byte2int(crc[0]) << 8) + byte2int(crc[1]) return checkCRC(data, crc_val) - except (IndexError, KeyError): + except (IndexError, KeyError, struct.error): return False def advanceFrame(self): @@ -243,9 +243,8 @@ def sendPacket(self, message): :param message: Message to be sent over the bus :return: """ - # _logger.debug("Current transaction state - {}".format( - # ModbusTransactionState.to_string(self.client.state)) - # ) + start = time.time() + timeout = start + self.client.timeout while self.client.state != ModbusTransactionState.IDLE: if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE: ts = round(time.time(), 6) @@ -267,8 +266,13 @@ def sendPacket(self, message): time.sleep(self.client.silent_interval) self.client.state = ModbusTransactionState.IDLE else: - _logger.debug("Sleeping") - time.sleep(self.client.silent_interval) + if time.time() > timeout: + _logger.debug("Spent more time than the read time out, " + "resetting the transaction to IDLE") + self.client.state = ModbusTransactionState.IDLE + else: + _logger.debug("Sleeping") + time.sleep(self.client.silent_interval) size = self.client.send(message) # if size: # _logger.debug("Changing transaction state from 'SENDING' " diff --git a/pymodbus/version.py b/pymodbus/version.py index 45f9f148a..25cd60b28 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, 0, 1) +version = Version('pymodbus', 2, 0, 2) version.__name__ = 'pymodbus' # fix epydoc error From cfd6640aa0d3eec30c6c1e71960d85bff06e0a56 Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Mon, 1 Oct 2018 18:11:23 +0530 Subject: [PATCH 07/11] Update changelog, change log formatter for repl verbose mode --- CHANGELOG.rst | 6 ++++++ pymodbus/repl/main.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 15e0b94a7..9c8db7fe1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,9 @@ +Version 2.0.2 +----------------------------------------------------------- +* Fix Infinite sleep loop in RTU Framer +* Add pygments as extra requirement for repl +* More verbose logs for repl + Version 2.0.1 ----------------------------------------------------------- * Fix unicode decoder error with BinaryPayloadDecoder in some platforms diff --git a/pymodbus/repl/main.py b/pymodbus/repl/main.py index eae6ceade..045f288e8 100644 --- a/pymodbus/repl/main.py +++ b/pymodbus/repl/main.py @@ -211,7 +211,8 @@ def main(ctx, verbose): if verbose: global log import logging - format = '%(asctime)-15s %(message)s' + format = ('%(asctime)-15s %(threadName)-15s ' + '%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') log = logging.getLogger('pymodbus') logging.basicConfig(format=format) log.setLevel(logging.DEBUG) From 68a8fd701bec1deda39d6344c9f56d5ab97ff672 Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Mon, 1 Oct 2018 18:11:41 +0530 Subject: [PATCH 08/11] Update unit test and requirements for repl --- setup.py | 3 ++- test/test_framers.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 14e78b2d3..9069eda16 100644 --- a/setup.py +++ b/setup.py @@ -92,7 +92,8 @@ ], 'repl': [ 'click>=6.7', - 'prompt-toolkit==2.0.4' + 'prompt-toolkit==2.0.4', + 'pygments==2.2.0' ] }, entry_points={ diff --git a/test/test_framers.py b/test/test_framers.py index c8dc037fe..4423bb9c3 100644 --- a/test/test_framers.py +++ b/test/test_framers.py @@ -132,10 +132,13 @@ def test_send_packet(rtu_framer): client.state = ModbusTransactionState.TRANSACTION_COMPLETE client.silent_interval = 1 client.last_frame_end = 1 + client.timeout = 0.25 client.idle_time.return_value = 1 client.send.return_value = len(message) rtu_framer.client = client assert rtu_framer.sendPacket(message) == len(message) + client.state = ModbusTransactionState.PROCESSING_REPLY + assert rtu_framer.sendPacket(message) == len(message) def test_recv_packet(rtu_framer): From 1585afc196eb390b9e8667bba832a0cc77f19b2b Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Wed, 3 Oct 2018 10:03:19 +0530 Subject: [PATCH 09/11] #307 Fix partial data read for serial client when the response size is unknown --- .gitignore | 2 +- CHANGELOG.rst | 7 ++++--- pymodbus/client/sync.py | 13 ++++++++++--- pymodbus/framer/rtu_framer.py | 5 ----- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 9fae94a63..426321af3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,4 @@ test/__pycache__/ /doc/html/ /doc/_build/ .pytest_cache/ -/.pymodhis +**/.pymodhis diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c8db7fe1..96d5d2944 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,9 @@ Version 2.0.2 ----------------------------------------------------------- -* Fix Infinite sleep loop in RTU Framer -* Add pygments as extra requirement for repl -* More verbose logs for repl +* Fix Issues with Serial client where in partial data was read when the response size is unknown. +* Fix Infinite sleep loop in RTU Framer. +* Add pygments as extra requirement for repl. +* More verbose logs for repl. Version 2.0.1 ----------------------------------------------------------- diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 8bf14df03..ca009fde4 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -512,15 +512,22 @@ def _send(self, request): return 0 def _wait_for_data(self): + size = 0 + more_data = False if self.timeout is not None and self.timeout != 0: - condition = partial(lambda start, timeout: (time.time() - start) <= timeout, timeout=self.timeout) + condition = partial(lambda start, timeout: + (time.time() - start) <= timeout, + timeout=self.timeout) else: condition = partial(lambda dummy1, dummy2: True, dummy2=None) start = time.time() while condition(start): - size = self._in_waiting() - if size: + avaialble = self._in_waiting() + if (more_data and not avaialble) or (more_data and avaialble == size): break + if avaialble and avaialble != size: + more_data = True + size = avaialble time.sleep(0.01) return size diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py index b54a17943..a4c052442 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/rtu_framer.py @@ -274,11 +274,6 @@ def sendPacket(self, message): _logger.debug("Sleeping") time.sleep(self.client.silent_interval) size = self.client.send(message) - # if size: - # _logger.debug("Changing transaction state from 'SENDING' " - # "to 'WAITING FOR REPLY'") - # self.client.state = ModbusTransactionState.WAITING_FOR_REPLY - self.client.last_frame_end = round(time.time(), 6) return size From 2a258cc32bf42553e265fa378d53699ed575fc0f Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Wed, 3 Oct 2018 15:01:03 +0530 Subject: [PATCH 10/11] v2.1.0 release --- CHANGELOG.rst | 4 +- doc/source/library/REPL.md | 84 ++++++++++++++++++++++++++++++++++++-- pymodbus/repl/README.md | 83 +++++++++++++++++++++++++++++++++++-- pymodbus/version.py | 2 +- 4 files changed, 164 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2747f83df..4053b2bfd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,10 @@ -Version 2.0.2 +Version 2.1.0 ----------------------------------------------------------- * Fix Issues with Serial client where in partial data was read when the response size is unknown. * Fix Infinite sleep loop in RTU Framer. * Add pygments as extra requirement for repl. +* Add support to modify modbus client attributes via repl. +* Update modbus repl documentation. * More verbose logs for repl. Version 2.0.1 diff --git a/doc/source/library/REPL.md b/doc/source/library/REPL.md index 7d6e77305..1e7969af6 100644 --- a/doc/source/library/REPL.md +++ b/doc/source/library/REPL.md @@ -48,7 +48,7 @@ Options: ``` -RTU Options +SERIAL Options ``` bash-3.2$ pymodbus.console serial --help Usage: pymodbus.console serial [OPTIONS] @@ -79,6 +79,7 @@ Options: To view all available commands type `help` +TCP ``` $ pymodbus.console tcp --host 192.168.128.126 --port 5020 @@ -129,6 +130,76 @@ client.write_register Write `value` to register at `addre client.write_registers Write list of `values` to registers starting at `address`. ``` +SERIAL +``` +$ pymodbus.console serial --port /dev/ttyUSB0 --baudrate 19200 --timeout 2 +> help +Available commands: +client.baudrate Read Only! +client.bytesize Read Only! +client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests. +client.clear_counters Diagnostic sub command, Clear all counters and diag registers. +client.clear_overrun_count Diagnostic sub command, Clear over run counter. +client.close Closes the underlying socket connection +client.connect Connect to the modbus serial server +client.debug_enabled Returns a boolean indicating if debug is enabled. +client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode. +client.get_baudrate Serial Port baudrate. +client.get_bytesize Number of data bits. +client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device. +client.get_com_event_counter Read status word and an event count from the remote device's communication event counter. +client.get_com_event_log Read status word, event count, message count, and a field of event bytes from the remote device. +client.get_parity Enable Parity Checking. +client.get_port Serial Port. +client.get_serial_settings Gets Current Serial port settings. +client.get_stopbits Number of stop bits. +client.get_timeout Serial Port Read timeout. +client.idle_time Bus Idle Time to initiate next transaction +client.inter_char_timeout Read Only! +client.is_socket_open c l i e n t . i s s o c k e t o p e n +client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask`. +client.method Read Only! +client.parity Read Only! +client.port Read Only! +client.read_coils Reads `count` coils from a given slave starting at `address`. +client.read_device_information Read the identification and additional information of remote slave. +client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address`. +client.read_exception_status Read the contents of eight Exception Status outputs in a remote device. +client.read_holding_registers Read `count` number of holding registers starting at `address`. +client.read_input_registers Read `count` number of input registers starting at `address`. +client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address`. +client.report_slave_id Report information about remote slave ID. +client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters . +client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave. +client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave. +client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave. +client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register. +client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave. +client.return_query_data Diagnostic sub command , Loop back data sent in response. +client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition. +client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave. +client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave. +client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave. +client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave. +client.set_baudrate Baudrate setter. +client.set_bytesize Byte size setter. +client.set_parity Parity Setter. +client.set_port Serial Port setter. +client.set_stopbits Stop bit setter. +client.set_timeout Read timeout setter. +client.silent_interval Read Only! +client.state Read Only! +client.stopbits Read Only! +client.timeout Read Only! +client.write_coil Write `value` to coil at `address`. +client.write_coils Write `value` to coil at `address`. +client.write_register Write `value` to register at `address`. +client.write_registers Write list of `values` to registers starting at `address`. +result.decode Decode the register response to known formatters. +result.raw Return raw result dict. + +``` + Every command has auto suggetion on the arguments supported , supply arg and value are to be supplied in `arg=val` format. ``` @@ -164,9 +235,14 @@ For Holding and Input register reads, the decoded value could be viewed with `re > ``` -Client settings could be retrived and altered as well. +Client settings could be retrieved and altered as well. ``` > # For serial settings + +> # Check the serial mode +> client.method +"rtu" + > client.get_serial_settings { "t1.5": 0.00171875, @@ -198,7 +274,7 @@ null ``` -#DEMO +## DEMO [![asciicast](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o.png)](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o) - +[![asciicast](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI.png)](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI) diff --git a/pymodbus/repl/README.md b/pymodbus/repl/README.md index 7d6e77305..2064f09d3 100644 --- a/pymodbus/repl/README.md +++ b/pymodbus/repl/README.md @@ -48,7 +48,7 @@ Options: ``` -RTU Options +SERIAL Options ``` bash-3.2$ pymodbus.console serial --help Usage: pymodbus.console serial [OPTIONS] @@ -79,6 +79,7 @@ Options: To view all available commands type `help` +TCP ``` $ pymodbus.console tcp --host 192.168.128.126 --port 5020 @@ -129,6 +130,76 @@ client.write_register Write `value` to register at `addre client.write_registers Write list of `values` to registers starting at `address`. ``` +SERIAL +``` +$ pymodbus.console serial --port /dev/ttyUSB0 --baudrate 19200 --timeout 2 +> help +Available commands: +client.baudrate Read Only! +client.bytesize Read Only! +client.change_ascii_input_delimiter Diagnostic sub command, Change message delimiter for future requests. +client.clear_counters Diagnostic sub command, Clear all counters and diag registers. +client.clear_overrun_count Diagnostic sub command, Clear over run counter. +client.close Closes the underlying socket connection +client.connect Connect to the modbus serial server +client.debug_enabled Returns a boolean indicating if debug is enabled. +client.force_listen_only_mode Diagnostic sub command, Forces the addressed remote device to its Listen Only Mode. +client.get_baudrate Serial Port baudrate. +client.get_bytesize Number of data bits. +client.get_clear_modbus_plus Diagnostic sub command, Get or clear stats of remote modbus plus device. +client.get_com_event_counter Read status word and an event count from the remote device's communication event counter. +client.get_com_event_log Read status word, event count, message count, and a field of event bytes from the remote device. +client.get_parity Enable Parity Checking. +client.get_port Serial Port. +client.get_serial_settings Gets Current Serial port settings. +client.get_stopbits Number of stop bits. +client.get_timeout Serial Port Read timeout. +client.idle_time Bus Idle Time to initiate next transaction +client.inter_char_timeout Read Only! +client.is_socket_open c l i e n t . i s s o c k e t o p e n +client.mask_write_register Mask content of holding register at `address` with `and_mask` and `or_mask`. +client.method Read Only! +client.parity Read Only! +client.port Read Only! +client.read_coils Reads `count` coils from a given slave starting at `address`. +client.read_device_information Read the identification and additional information of remote slave. +client.read_discrete_inputs Reads `count` number of discrete inputs starting at offset `address`. +client.read_exception_status Read the contents of eight Exception Status outputs in a remote device. +client.read_holding_registers Read `count` number of holding registers starting at `address`. +client.read_input_registers Read `count` number of input registers starting at `address`. +client.readwrite_registers Read `read_count` number of holding registers starting at `read_address` and write `write_registers` starting at `write_address`. +client.report_slave_id Report information about remote slave ID. +client.restart_comm_option Diagnostic sub command, initialize and restart remote devices serial interface and clear all of its communications event counters . +client.return_bus_com_error_count Diagnostic sub command, Return count of CRC errors received by remote slave. +client.return_bus_exception_error_count Diagnostic sub command, Return count of Modbus exceptions returned by remote slave. +client.return_bus_message_count Diagnostic sub command, Return count of message detected on bus by remote slave. +client.return_diagnostic_register Diagnostic sub command, Read 16-bit diagnostic register. +client.return_iop_overrun_count Diagnostic sub command, Return count of iop overrun errors by remote slave. +client.return_query_data Diagnostic sub command , Loop back data sent in response. +client.return_slave_bus_char_overrun_count Diagnostic sub command, Return count of messages not handled by remote slave due to character overrun condition. +client.return_slave_busy_count Diagnostic sub command, Return count of server busy exceptions sent by remote slave. +client.return_slave_message_count Diagnostic sub command, Return count of messages addressed to remote slave. +client.return_slave_no_ack_count Diagnostic sub command, Return count of NO ACK exceptions sent by remote slave. +client.return_slave_no_response_count Diagnostic sub command, Return count of No responses by remote slave. +client.set_baudrate Baudrate setter. +client.set_bytesize Byte size setter. +client.set_parity Parity Setter. +client.set_port Serial Port setter. +client.set_stopbits Stop bit setter. +client.set_timeout Read timeout setter. +client.silent_interval Read Only! +client.state Read Only! +client.stopbits Read Only! +client.timeout Read Only! +client.write_coil Write `value` to coil at `address`. +client.write_coils Write `value` to coil at `address`. +client.write_register Write `value` to register at `address`. +client.write_registers Write list of `values` to registers starting at `address`. +result.decode Decode the register response to known formatters. +result.raw Return raw result dict. + +``` + Every command has auto suggetion on the arguments supported , supply arg and value are to be supplied in `arg=val` format. ``` @@ -164,9 +235,14 @@ For Holding and Input register reads, the decoded value could be viewed with `re > ``` -Client settings could be retrived and altered as well. +Client settings could be retrieved and altered as well. ``` > # For serial settings + +> # Check the serial mode +> client.method +"rtu" + > client.get_serial_settings { "t1.5": 0.00171875, @@ -198,7 +274,8 @@ null ``` -#DEMO +## DEMO [![asciicast](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o.png)](https://asciinema.org/a/y1xOk7lm59U1bRBE2N1pDIj2o) +[![asciicast](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI.png)](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI) diff --git a/pymodbus/version.py b/pymodbus/version.py index 25cd60b28..7a997315e 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, 0, 2) +version = Version('pymodbus', 2, 1, 0) version.__name__ = 'pymodbus' # fix epydoc error From dc055a23912b6b9a844e38d55f28ccc20fbc56ff Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Wed, 3 Oct 2018 15:13:36 +0530 Subject: [PATCH 11/11] Update conf.py for docs --- doc/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 5eedee1f1..a9063dc13 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -21,6 +21,7 @@ import recommonmark from recommonmark.parser import CommonMarkParser from recommonmark.transform import AutoStructify +from pymodbus import __version__ parent_dir = os.path.abspath(os.pardir) # examples = os.path.join(parent_dir, "examples") example_contrib = os.path.join(parent_dir, "examples/contrib") @@ -72,9 +73,9 @@ # built documents. # # The short X.Y version. -version = u'1.4.0' +version = __version__ # The full version, including alpha/beta/rc tags. -release = u'1.4.0' +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages.