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 15e0b94a7..4053b2bfd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,12 @@ +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 ----------------------------------------------------------- * Fix unicode decoder error with BinaryPayloadDecoder in some platforms @@ -36,13 +45,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..a9063dc13 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,6 +18,10 @@ # import os import sys +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") @@ -31,7 +35,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 +45,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 +53,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' @@ -65,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. @@ -176,4 +184,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..1e7969af6 --- /dev/null +++ b/doc/source/library/REPL.md @@ -0,0 +1,280 @@ +# 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: + serial + 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. + + + + +``` + +SERIAL Options +``` +bash-3.2$ pymodbus.console serial --help +Usage: pymodbus.console serial [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` + +TCP +``` +$ 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.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.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`. +``` + +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. +``` + +> 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 + +> +``` + +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, + "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) +[![asciicast](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI.png)](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI) diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 8bf14df03..abfd724e0 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 @@ -424,7 +432,9 @@ def __init__(self, method='ascii', **kwargs): if self.baudrate > 19200: 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 @@ -463,6 +473,7 @@ def connect(self): _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,15 +523,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/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/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 1a46811a1..4c343dbf9 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): @@ -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) """ @@ -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) @@ -253,7 +252,7 @@ def sendPacket(self, message): "Current Time stamp - {}".format( self.client.last_frame_end, ts) ) - + if self.client.last_frame_end: idle_time = self.client.idle_time() if round(ts - idle_time, 6) <= self.client.silent_interval: @@ -267,14 +266,14 @@ 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' " - # "to 'WAITING FOR REPLY'") - # self.client.state = ModbusTransactionState.WAITING_FOR_REPLY - self.client.last_frame_end = round(time.time(), 6) return size 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/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/README.md b/pymodbus/repl/README.md index 7097dcdcd..2064f09d3 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 @@ -48,10 +48,10 @@ Options: ``` -RTU Options +SERIAL 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) @@ -79,45 +79,125 @@ Options: To view all available commands type `help` +TCP ``` $ 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`. +``` + +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. @@ -155,7 +235,47 @@ For Holding and Input register reads, the decoded value could be viewed with `re > ``` -#DEMO +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, + "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) +[![asciicast](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI.png)](https://asciinema.org/a/edUqZN77fdjxL2toisiilJNwI) 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 8a427e014..33602606c 100644 --- a/pymodbus/repl/client.py +++ b/pymodbus/repl/client.py @@ -1,10 +1,13 @@ """ +Modbus Clients to be used with REPL. + Copyright (c) 2018 Riptide IO, Inc. All Rights Reserved. """ 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,19 +39,30 @@ 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): """ - 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 @@ -67,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 @@ -81,11 +96,11 @@ 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): """ - 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 @@ -101,11 +116,11 @@ 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): """ - 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) @@ -121,11 +136,12 @@ 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): """ - 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 @@ -140,11 +156,12 @@ 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): """ - 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 @@ -159,11 +176,12 @@ 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): """ - 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 @@ -177,11 +195,12 @@ 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): """ - 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 @@ -195,13 +214,14 @@ 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): """ - 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 @@ -223,12 +243,13 @@ 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): """ - 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 @@ -246,12 +267,12 @@ 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): """ - 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. @@ -271,11 +292,12 @@ 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): """ - Report information about remote slave ID + Report information about remote slave ID. + :param unit: The slave unit this request is targeting :return: """ @@ -284,16 +306,18 @@ 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 } else: - return str(resp) + return ExtendedRequestSupport._process_exception(resp) 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: """ @@ -305,12 +329,13 @@ 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): """ - 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: """ @@ -323,7 +348,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 +369,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,11 +380,12 @@ 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): """ - 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: @@ -369,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: @@ -380,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 @@ -391,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 @@ -402,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 @@ -414,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 @@ -425,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 @@ -437,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 @@ -449,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 @@ -461,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 @@ -473,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 @@ -484,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 @@ -496,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 @@ -508,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 @@ -520,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 @@ -532,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 @@ -543,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 @@ -557,6 +585,132 @@ class ModbusSerialClient(ExtendedRequestSupport, _ModbusSerialClient): def __init__(self, method, **kwargs): super(ModbusSerialClient, self).__init__(method, **kwargs) + def get_port(self): + """ + Serial Port. + + :return: Current Serial port + """ + return self.port + + def set_port(self, value): + """ + Serial Port setter. + + :param value: New port + """ + self.port = value + if self.is_socket_open(): + self.close() + + def get_stopbits(self): + """ + Number of stop bits. + + :return: Current Stop bits + """ + return self.stopbits + + def set_stopbits(self, value): + """ + Stop bit setter. + + :param value: Possible values (1, 1.5, 2) + """ + self.stopbits = float(value) + if self.is_socket_open(): + self.close() + + def get_bytesize(self): + """ + Number of data bits. + + :return: Current bytesize + """ + return self.bytesize + + def set_bytesize(self, value): + """ + Byte size setter. + + :param value: Possible values (5, 6, 7, 8) + + """ + self.bytesize = int(value) + if self.is_socket_open(): + self.close() + + def get_parity(self): + """ + Enable Parity Checking. + + :return: Current parity setting + """ + return self.parity + + def set_parity(self, value): + """ + Parity Setter. + + :param value: Possible values ('N', 'E', 'O', 'M', 'S') + """ + self.parity = value + if self.is_socket_open(): + self.close() + + def get_baudrate(self): + """ + Serial Port baudrate. + + :return: Current baudrate + """ + return self.baudrate + + def set_baudrate(self, value): + """ + Baudrate setter. + + :param value: + """ + self.baudrate = int(value) + if self.is_socket_open(): + self.close() + + def get_timeout(self): + """ + Serial Port Read timeout. + + :return: Current read imeout. + """ + return self.timeout + + def set_timeout(self, value): + """ + Read timeout setter. + + :param value: Read Timeout in seconds + """ + self.timeout = float(value) + if self.is_socket_open(): + self.close() + + def get_serial_settings(self): + """ + Gets Current Serial port settings. + + :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, + '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..391c245a1 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,15 +29,18 @@ 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, commands=None, ignore_case=True, **kwargs): - self._commands = commands or get_commands() + def __init__(self, client, commands=None, ignore_case=True): + """ + + :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() self.ignore_case = ignore_case @@ -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 != '': @@ -79,13 +82,12 @@ def completing_arg(self, words, word_before_cursor): return False def arg_completions(self, words, word_before_cursor): - """Generates arguments completions based on the input. - :type words: list + """ + 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, + :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 +101,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 +132,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 4ec729481..eff1023c7 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. """ @@ -7,7 +9,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 +16,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 +46,43 @@ "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', '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 = [] class Command(object): + """ + Class representing Commands to be consumed by Completer. + """ + def __init__(self, name, signature, doc, unit=False): + """ - def __init__(self, name, signature, doc): + :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() 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): @@ -79,6 +102,11 @@ def _create_arg_help(self): return param_dict def create_completion(self): + """ + Create command completion meta data. + + :return: + """ words = {} def _create(entry, default): @@ -111,9 +139,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, '') @@ -124,14 +163,60 @@ 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])) + 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]), 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): + """ + 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) + 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,18 +225,34 @@ 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): + """ + Represent result command. + """ + function_code = None + data = None + def __init__(self, result): - self.function_code = result.pop('function_code', None) - self.data = dict(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) + else: + self.data = 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 @@ -186,7 +287,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() @@ -205,6 +307,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 eae6ceade..d0501322c 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. """ @@ -25,7 +27,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 @@ -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() @@ -147,7 +165,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 +190,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() @@ -211,7 +231,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) @@ -235,8 +256,7 @@ def tcp(ctx, host, port): cli(client) - -@main.command("rtu") +@main.command("serial") @click.pass_context @click.option( "--method", @@ -314,8 +334,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, 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/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/pymodbus/version.py b/pymodbus/version.py index 45f9f148a..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, 1) +version = Version('pymodbus', 2, 1, 0) version.__name__ = 'pymodbus' # fix epydoc error diff --git a/requirements-docs.txt b/requirements-docs.txt index 0457415a0..f7abfb945 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 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):