From 713fe5f01d9a62a49089135b9768ebf62d6668e6 Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Sat, 13 Jan 2018 11:22:57 +0530 Subject: [PATCH 1/5] Modbus sync client timing enhancements #221 Fix TCP server #256, #260 --- CHANGELOG.rst | 10 + examples/common/synchronous_server.py | 35 ++-- pymodbus/client/common.py | 13 ++ pymodbus/client/sync.py | 288 +++++++++++++++----------- pymodbus/datastore/context.py | 38 ++-- pymodbus/server/async.py | 7 +- pymodbus/server/sync.py | 276 +++++++++++++----------- pymodbus/transaction.py | 236 +++++++++++++-------- pymodbus/utilities.py | 10 + pymodbus/version.py | 2 +- test/test_client_sync.py | 6 + test/test_server_async.py | 5 +- test/test_server_sync.py | 7 +- 13 files changed, 563 insertions(+), 370 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a2fffd0c9..8822c5932 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,13 @@ +Version 1.5.0 +------------------------------------------------------------ +* Improve transaction speeds for sync clients (RTU/ASCII), now retry on empty happens only when retry_on_empty kwarg is passed to client during intialization + +`client = Client(..., retry_on_empty=True)` + +* Fix tcp servers (sync/async) not processing requests with transaction id > 255 +* Introduce new api to check if the received response is an error or not (response.isError()) +* Fix Misc examples + Version 1.4.0 ------------------------------------------------------------ * Bug fix Modbus TCP client reading incomplete data diff --git a/examples/common/synchronous_server.py b/examples/common/synchronous_server.py index e5ad9486e..ef08175e1 100755 --- a/examples/common/synchronous_server.py +++ b/examples/common/synchronous_server.py @@ -19,15 +19,19 @@ from pymodbus.datastore import ModbusSequentialDataBlock from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext -from pymodbus.transaction import ModbusRtuFramer +from pymodbus.transaction import ModbusRtuFramer, ModbusBinaryFramer # --------------------------------------------------------------------------- # # configure the service logging # --------------------------------------------------------------------------- # import logging -logging.basicConfig() +FORMAT = '%(asctime)-15s %(levelname)-8s %(module)-8s:%(lineno)-8s %(message)s' +logging.basicConfig(format=FORMAT) log = logging.getLogger() log.setLevel(logging.DEBUG) +SERIAL_PORT = "/tmp/ptyp0" +TCP_PORT = 5020 + def run_server(): # ----------------------------------------------------------------------- # @@ -85,11 +89,11 @@ def run_server(): # store = ModbusSlaveContext(..., zero_mode=True) # ----------------------------------------------------------------------- # store = ModbusSlaveContext( - di = ModbusSequentialDataBlock(0, [17]*100), - co = ModbusSequentialDataBlock(0, [17]*100), - hr = ModbusSequentialDataBlock(0, [17]*100), - ir = ModbusSequentialDataBlock(0, [17]*100)) - context = ModbusServerContext(slaves=store, single=True) + di=ModbusSequentialDataBlock(0, [17]*100), + co=ModbusSequentialDataBlock(0, [17]*100), + hr=ModbusSequentialDataBlock(0, [17]*100), + ir=ModbusSequentialDataBlock(0, [17]*100)) + context = ModbusServerContext(slaves={1: store, 2: store}, single=False) # ----------------------------------------------------------------------- # # initialize the server information @@ -108,18 +112,23 @@ def run_server(): # run the server you want # ----------------------------------------------------------------------- # # Tcp: - StartTcpServer(context, identity=identity, address=("localhost", 5020)) + StartTcpServer(context, identity=identity, address=("localhost", TCP_PORT)) # Udp: - # StartUdpServer(context, identity=identity, address=("localhost", 502)) + # StartUdpServer(context, identity=identity, + # address=("localhost", TCP_PORT)) # Ascii: - # StartSerialServer(context, identity=identity, - # port='/dev/pts/3', timeout=1) - + # StartSerialServer(context, identity=identity, + # port=SERIAL_PORT, timeout=1) + + # Binary: + # StartSerialServer(context, identity=identity, framer=ModbusBinaryFramer, + # port=SERIAL_PORT, timeout=1) + # RTU: # StartSerialServer(context, framer=ModbusRtuFramer, identity=identity, - # port='/dev/ptyp0', timeout=.005, baudrate=9600) + # port=SERIAL_PORT, timeout=.005, baudrate=9600) if __name__ == "__main__": diff --git a/pymodbus/client/common.py b/pymodbus/client/common.py index 4e2f4bde3..b740da9c5 100644 --- a/pymodbus/client/common.py +++ b/pymodbus/client/common.py @@ -15,6 +15,19 @@ from pymodbus.other_message import * +class ModbusTransactionState(object): + """ + Modbus Client States + """ + IDLE = 0 + SENDING = 1 + WAITING_FOR_REPLY = 2 + WAITING_TURNAROUND_DELAY = 3 + PROCESSING_REPLY = 4 + PROCESSING_ERROR = 5 + TRANSCATION_COMPLETE = 6 + + class ModbusClientMixin(object): ''' This is a modbus client mixin that provides additional factory diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 80946b77a..fb214297a 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -3,121 +3,133 @@ import time from pymodbus.constants import Defaults +from pymodbus.utilities import hexlify_packets from pymodbus.factory import ClientDecoder -from pymodbus.compat import byte2int from pymodbus.exceptions import NotImplementedException, ParameterException from pymodbus.exceptions import ConnectionException from pymodbus.transaction import FifoTransactionManager from pymodbus.transaction import DictTransactionManager from pymodbus.transaction import ModbusSocketFramer, ModbusBinaryFramer from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer -from pymodbus.client.common import ModbusClientMixin +from pymodbus.client.common import ModbusClientMixin, ModbusTransactionState -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Logging -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # import logging _logger = logging.getLogger(__name__) -#---------------------------------------------------------------------------# +TransactionStateString = { + ModbusTransactionState.IDLE: "IDLE", + ModbusTransactionState.SENDING: "SENDING", + ModbusTransactionState.WAITING_FOR_REPLY: "WAITING_FOR_REPLY", + ModbusTransactionState.WAITING_TURNAROUND_DELAY: "WAITING_TURNAROUND_DELAY", + ModbusTransactionState.PROCESSING_REPLY: "PROCESSING_REPLY", + ModbusTransactionState.PROCESSING_ERROR: "PROCESSING_ERROR", + ModbusTransactionState.TRANSCATION_COMPLETE: "TRANSCATION_COMPLETE" +} + +# --------------------------------------------------------------------------- # # The Synchronous Clients -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # + class BaseModbusClient(ModbusClientMixin): - ''' + """ Inteface for a modbus synchronous client. Defined here are all the methods for performing the related request methods. Derived classes simply need to implement the transport methods and set the correct framer. - ''' + """ def __init__(self, framer, **kwargs): - ''' Initialize a client instance + """ Initialize a client instance :param framer: The modbus framer implementation to use - ''' + """ self.framer = framer if isinstance(self.framer, ModbusSocketFramer): self.transaction = DictTransactionManager(self, **kwargs) - else: self.transaction = FifoTransactionManager(self, **kwargs) + else: + self.transaction = FifoTransactionManager(self, **kwargs) - #-----------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # Client interface - #-----------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def connect(self): - ''' Connect to the modbus remote host + """ Connect to the modbus remote host :returns: True if connection succeeded, False otherwise - ''' + """ raise NotImplementedException("Method not implemented by derived class") def close(self): - ''' Closes the underlying socket connection - ''' + """ Closes the underlying socket connection + """ pass def _send(self, request): - ''' Sends data on the underlying socket + """ Sends data on the underlying socket :param request: The encoded request to send :return: The number of bytes written - ''' + """ raise NotImplementedException("Method not implemented by derived class") def _recv(self, size): - ''' Reads data from the underlying descriptor + """ Reads data from the underlying descriptor :param size: The number of bytes to read :return: The bytes read - ''' + """ raise NotImplementedException("Method not implemented by derived class") - #-----------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # Modbus client methods - #-----------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def execute(self, request=None): - ''' + """ :param request: The request to process :returns: The result of the request execution - ''' + """ if not self.connect(): raise ConnectionException("Failed to connect[%s]" % (self.__str__())) return self.transaction.execute(request) - #-----------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # The magic methods - #-----------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def __enter__(self): - ''' Implement the client with enter block + """ Implement the client with enter block :returns: The current instance of the client - ''' + """ if not self.connect(): raise ConnectionException("Failed to connect[%s]" % (self.__str__())) return self def __exit__(self, klass, value, traceback): - ''' Implement the client with exit block ''' + """ Implement the client with exit block """ self.close() def __str__(self): - ''' Builds a string representation of the connection + """ Builds a string representation of the connection :returns: The string representation - ''' + """ return "Null Transport" -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Modbus TCP Client Transport Implementation -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusTcpClient(BaseModbusClient): - ''' Implementation of a modbus tcp client - ''' + """ Implementation of a modbus tcp client + """ def __init__(self, host='127.0.0.1', port=Defaults.Port, framer=ModbusSocketFramer, **kwargs): - ''' Initialize a client instance + """ Initialize a client instance :param host: The host to connect to (default 127.0.0.1) :param port: The modbus port to connect to (default 502) @@ -126,42 +138,44 @@ def __init__(self, host='127.0.0.1', port=Defaults.Port, :param framer: The modbus framer to use (default ModbusSocketFramer) .. note:: The host argument will accept ipv4 and ipv6 hosts - ''' + """ self.host = host self.port = port self.source_address = kwargs.get('source_address', ('', 0)) self.socket = None - self.timeout = kwargs.get('timeout', Defaults.Timeout) + self.timeout = kwargs.get('timeout', Defaults.Timeout) BaseModbusClient.__init__(self, framer(ClientDecoder()), **kwargs) def connect(self): - ''' Connect to the modbus tcp server + """ Connect to the modbus tcp server :returns: True if connection succeeded, False otherwise - ''' + """ if self.socket: return True try: - self.socket = socket.create_connection((self.host, self.port), - timeout=self.timeout, source_address=self.source_address) + self.socket = socket.create_connection( + (self.host, self.port), + timeout=self.timeout, + source_address=self.source_address) except socket.error as msg: - _logger.error('Connection to (%s, %s) failed: %s' % \ - (self.host, self.port, msg)) + _logger.error('Connection to (%s, %s) ' + 'failed: %s' % (self.host, self.port, msg)) self.close() - return self.socket != None + return self.socket is not None def close(self): - ''' Closes the underlying socket connection - ''' + """ Closes the underlying socket connection + """ if self.socket: self.socket.close() self.socket = None def _send(self, request): - ''' Sends data on the underlying socket + """ Sends data on the underlying socket :param request: The encoded request to send :return: The number of bytes written - ''' + """ if not self.socket: raise ConnectionException(self.__str__()) if request: @@ -169,53 +183,53 @@ def _send(self, request): return 0 def _recv(self, size): - ''' Reads data from the underlying descriptor + """ Reads data from the underlying descriptor :param size: The number of bytes to read :return: The bytes read - ''' + """ if not self.socket: raise ConnectionException(self.__str__()) return self.socket.recv(size) def __str__(self): - ''' Builds a string representation of the connection + """ Builds a string representation of the connection :returns: The string representation - ''' + """ return "%s:%s" % (self.host, self.port) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Modbus UDP Client Transport Implementation -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusUdpClient(BaseModbusClient): - ''' Implementation of a modbus udp client - ''' + """ Implementation of a modbus udp client + """ def __init__(self, host='127.0.0.1', port=Defaults.Port, - framer=ModbusSocketFramer, **kwargs): - ''' Initialize a client instance + framer=ModbusSocketFramer, **kwargs): + """ Initialize a client instance :param host: The host to connect to (default 127.0.0.1) :param port: The modbus port to connect to (default 502) :param framer: The modbus framer to use (default ModbusSocketFramer) :param timeout: The timeout to use for this socket (default None) - ''' - self.host = host - self.port = port - self.socket = None + """ + self.host = host + self.port = port + self.socket = None self.timeout = kwargs.get('timeout', None) BaseModbusClient.__init__(self, framer(ClientDecoder()), **kwargs) @classmethod def _get_address_family(cls, address): - ''' A helper method to get the correct address family + """ A helper method to get the correct address family for a given address. :param address: The address to get the af for :returns: AF_INET for ipv4 and AF_INET6 for ipv6 - ''' + """ try: _ = socket.inet_pton(socket.AF_INET6, address) except socket.error: # not a valid ipv6 address @@ -223,11 +237,12 @@ def _get_address_family(cls, address): return socket.AF_INET6 def connect(self): - ''' Connect to the modbus tcp server + """ Connect to the modbus tcp server :returns: True if connection succeeded, False otherwise - ''' - if self.socket: return True + """ + if self.socket: + return True try: family = ModbusUdpClient._get_address_family(self.host) self.socket = socket.socket(family, socket.SOCK_DGRAM) @@ -235,19 +250,19 @@ def connect(self): except socket.error as ex: _logger.error('Unable to create udp socket %s' % ex) self.close() - return self.socket != None + return self.socket is not None def close(self): - ''' Closes the underlying socket connection - ''' + """ Closes the underlying socket connection + """ self.socket = None def _send(self, request): - ''' Sends data on the underlying socket + """ Sends data on the underlying socket :param request: The encoded request to send :return: The number of bytes written - ''' + """ if not self.socket: raise ConnectionException(self.__str__()) if request: @@ -255,32 +270,33 @@ def _send(self, request): return 0 def _recv(self, size): - ''' Reads data from the underlying descriptor + """ Reads data from the underlying descriptor :param size: The number of bytes to read :return: The bytes read - ''' + """ if not self.socket: raise ConnectionException(self.__str__()) return self.socket.recvfrom(size)[0] def __str__(self): - ''' Builds a string representation of the connection + """ Builds a string representation of the connection :returns: The string representation - ''' + """ return "%s:%s" % (self.host, self.port) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Modbus Serial Client Transport Implementation -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusSerialClient(BaseModbusClient): - ''' Implementation of a modbus serial client - ''' + """ Implementation of a modbus serial client + """ + state = ModbusTransactionState.IDLE def __init__(self, method='ascii', **kwargs): - ''' Initialize a serial client instance + """ Initialize a serial client instance The methods to connect are:: @@ -295,17 +311,18 @@ def __init__(self, method='ascii', **kwargs): :param parity: Which kind of parity to use :param baudrate: The baud rate to use for the serial device :param timeout: The timeout between serial requests (default 3s) - ''' - self.method = method - self.socket = None - BaseModbusClient.__init__(self, self.__implementation(method), **kwargs) + """ + self.method = method + self.socket = None + BaseModbusClient.__init__(self, self.__implementation(method), + **kwargs) - self.port = kwargs.get('port', 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.parity = kwargs.get('parity', Defaults.Parity) self.baudrate = kwargs.get('baudrate', Defaults.Baudrate) - self.timeout = kwargs.get('timeout', Defaults.Timeout) + self.timeout = kwargs.get('timeout', Defaults.Timeout) if self.method == "rtu": self._last_frame_end = 0.0 if self.baudrate > 19200: @@ -315,44 +332,51 @@ def __init__(self, method='ascii', **kwargs): @staticmethod def __implementation(method): - ''' Returns the requested framer + """ Returns the requested framer :method: The serial framer to instantiate :returns: The requested serial framer - ''' + """ method = method.lower() - if method == 'ascii': return ModbusAsciiFramer(ClientDecoder()) - elif method == 'rtu': return ModbusRtuFramer(ClientDecoder()) - elif method == 'binary': return ModbusBinaryFramer(ClientDecoder()) - elif method == 'socket': return ModbusSocketFramer(ClientDecoder()) + if method == 'ascii': + return ModbusAsciiFramer(ClientDecoder()) + elif method == 'rtu': + return ModbusRtuFramer(ClientDecoder()) + elif method == 'binary': + return ModbusBinaryFramer(ClientDecoder()) + elif method == 'socket': + return ModbusSocketFramer(ClientDecoder()) raise ParameterException("Invalid framer method requested") def connect(self): - ''' Connect to the modbus serial server + """ Connect to the modbus serial server :returns: True if connection succeeded, False otherwise - ''' + """ 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._last_frame_end = time.time() - return self.socket != None + self._last_frame_end = None + return self.socket is not None def close(self): - ''' Closes the underlying socket connection - ''' + """ Closes the underlying socket connection + """ if self.socket: self.socket.close() self.socket = None def _send(self, request): - ''' Sends data on the underlying socket + """ Sends data on the underlying socket If receive buffer still holds some data then flush it. @@ -361,18 +385,35 @@ def _send(self, request): :param request: The encoded request to send :return: The number of bytes written - ''' + """ + _logger.debug("Current transaction " + "state - {}".format(TransactionStateString[self.state])) + while self.state != ModbusTransactionState.IDLE: + if self.state == ModbusTransactionState.TRANSCATION_COMPLETE: + self.state = ModbusTransactionState.IDLE + else: + time.sleep(self._silent_interval) + _logger.debug("Transaction state 'IDLE', intiating a new transaction") + self.state = ModbusTransactionState.SENDING if not self.socket: raise ConnectionException(self.__str__()) if request: ts = time.time() if self.method == "rtu": - if ts < self._last_frame_end + self._silent_interval: - _logger.debug("will sleep to wait for 3.5 char") - time.sleep(self._last_frame_end + self._silent_interval - ts) + if self._last_frame_end: + if ts < self._last_frame_end + self._silent_interval: + _logger.debug("waiting for 3.5 char before next " + "send".format(self._silent_interval)) + time.sleep( + self._last_frame_end + self._silent_interval - ts + ) + _logger.debug("waited for - {} " + "ms".format(self._silent_interval*1000)) try: - in_waiting = "in_waiting" if hasattr(self.socket, "in_waiting") else "inWaiting" + in_waiting = ("in_waiting" if hasattr( + self.socket, "in_waiting") else "inWaiting") + if in_waiting == "in_waiting": waitingbytes = getattr(self.socket, in_waiting) else: @@ -380,39 +421,48 @@ def _send(self, request): if waitingbytes: result = self.socket.read(waitingbytes) if _logger.isEnabledFor(logging.WARNING): - _logger.warning("cleanup recv buffer before send: " + " ".join([hex(byte2int(x)) for x in result])) + _logger.warning("Cleanup recv buffer before " + "send: " + hexlify_packets(result)) except NotImplementedError: pass size = self.socket.write(request) + _logger.debug("Changing transaction state from 'SENDING' " + "to 'WAITING FOR REPLY'") + self.state = ModbusTransactionState.WAITING_FOR_REPLY if self.method == "rtu": self._last_frame_end = time.time() return size return 0 def _recv(self, size): - ''' Reads data from the underlying descriptor + """ Reads data from the underlying descriptor :param size: The number of bytes to read :return: The bytes read - ''' + """ if not self.socket: raise ConnectionException(self.__str__()) result = self.socket.read(size) + _logger.debug("Changing transaction state from 'WAITING FOR REPLY' " + "to 'PROCESSING REPLY'") + self.state = ModbusTransactionState.PROCESSING_REPLY if self.method == "rtu": self._last_frame_end = time.time() return result def __str__(self): - ''' Builds a string representation of the connection + """ Builds a string representation of the connection :returns: The string representation - ''' + """ return "%s baud[%s]" % (self.method, self.baudrate) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Exported symbols -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # + + __all__ = [ "ModbusTcpClient", "ModbusUdpClient", "ModbusSerialClient" -] \ No newline at end of file +] diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index a5e311a34..b80b3ceb4 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -31,7 +31,7 @@ def __init__(self, *args, **kwargs): 'hr' - Holding Register initializer 'ir' - Input Registers iniatializer ''' - self.store = {} + self.store = dict() self.store['d'] = kwargs.get('di', ModbusSequentialDataBlock.create()) self.store['c'] = kwargs.get('co', ModbusSequentialDataBlock.create()) self.store['i'] = kwargs.get('ir', ModbusSequentialDataBlock.create()) @@ -100,10 +100,10 @@ def __init__(self, slaves=None, single=True): :param slaves: A dictionary of client contexts :param single: Set to true to treat this as a single context ''' - self.single = single - self.__slaves = slaves or {} + self.single = single + self._slaves = slaves or {} if self.single: - self.__slaves = {Defaults.UnitId: self.__slaves} + self._slaves = {Defaults.UnitId: self._slaves} def __iter__(self): ''' Iterater over the current collection of slave @@ -111,7 +111,7 @@ def __iter__(self): :returns: An iterator over the slave contexts ''' - return iteritems(self.__slaves) + return iteritems(self._slaves) def __contains__(self, slave): ''' Check if the given slave is in this list @@ -119,7 +119,10 @@ def __contains__(self, slave): :param slave: slave The slave to check for existance :returns: True if the slave exists, False otherwise ''' - return slave in self.__slaves + if self.single and self._slaves: + return True + else: + return slave in self._slaves def __setitem__(self, slave, context): ''' Used to set a new slave context @@ -127,11 +130,13 @@ def __setitem__(self, slave, context): :param slave: The slave context to set :param context: The new context to set for this slave ''' - if self.single: slave = Defaults.UnitId + if self.single: + slave = Defaults.UnitId if 0xf7 >= slave >= 0x00: - self.__slaves[slave] = context + self._slaves[slave] = context else: - raise NoSuchSlaveException('slave index :{} out of range'.format(slave)) + raise NoSuchSlaveException('slave index :{} ' + 'out of range'.format(slave)) def __delitem__(self, slave): ''' Wrapper used to access the slave context @@ -139,9 +144,10 @@ def __delitem__(self, slave): :param slave: The slave context to remove ''' if not self.single and (0xf7 >= slave >= 0x00): - del self.__slaves[slave] + del self._slaves[slave] else: - raise NoSuchSlaveException('slave index: {} out of range'.format(slave)) + raise NoSuchSlaveException('slave index: {} ' + 'out of range'.format(slave)) def __getitem__(self, slave): ''' Used to get access to a slave context @@ -149,8 +155,10 @@ def __getitem__(self, slave): :param slave: The slave context to get :returns: The requested slave context ''' - if self.single: slave = Defaults.UnitId - if slave in self.__slaves: - return self.__slaves.get(slave) + if self.single: + slave = Defaults.UnitId + if slave in self._slaves: + return self._slaves.get(slave) else: - raise NoSuchSlaveException("slave - {} does not exist, or is out of range".format(slave)) + raise NoSuchSlaveException("slave - {} does not exist, " + "or is out of range".format(slave)) diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 5dfdc1f65..94921d5c3 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -9,6 +9,7 @@ from twisted.internet import reactor from pymodbus.constants import Defaults +from pymodbus.utilities import hexlify_packets from pymodbus.factory import ServerDecoder from pymodbus.datastore import ModbusServerContext from pymodbus.device import ModbusControlBlock @@ -57,9 +58,9 @@ def dataReceived(self, data): :param data: The data sent by the client ''' if _logger.isEnabledFor(logging.DEBUG): - _logger.debug(' '.join([hex(byte2int(x)) for x in data])) + _logger.debug('Data Received: ' + hexlify_packets(data)) if not self.factory.control.ListenOnly: - unit_address = byte2int(data[0]) + unit_address = byte2int(data[6]) if unit_address in self.factory.store: self.framer.processIncomingPacket(data, self._execute) @@ -163,7 +164,7 @@ def datagramReceived(self, data, addr): ''' _logger.debug("Client Connected [%s]" % addr) if _logger.isEnabledFor(logging.DEBUG): - _logger.debug(" ".join([hex(byte2int(x)) for x in data])) + _logger.debug("Datagram Received: "+ hexlify_packets(data)) if not self.control.ListenOnly: continuation = lambda request: self._execute(request, addr) self.framer.processIncomingPacket(data, continuation) diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 7941701d6..2a9c6c3c9 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -1,14 +1,15 @@ -''' +""" Implementation of a Threaded Modbus Server ------------------------------------------ -''' +""" from binascii import b2a_hex import serial import socket import traceback from pymodbus.constants import Defaults +from pymodbus.utilities import hexlify_packets from pymodbus.factory import ServerDecoder from pymodbus.datastore import ModbusServerContext from pymodbus.device import ModbusControlBlock @@ -18,43 +19,45 @@ from pymodbus.pdu import ModbusExceptions as merror from pymodbus.compat import socketserver, byte2int -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Logging -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # import logging _logger = logging.getLogger(__name__) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Protocol Handlers -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusBaseRequestHandler(socketserver.BaseRequestHandler): - ''' Implements the modbus server protocol + """ Implements the modbus server protocol This uses the socketserver.BaseRequestHandler to implement the client handler. - ''' - + """ + running = False + framer = None + def setup(self): - ''' Callback for when a client connects - ''' + """ Callback for when a client connects + """ _logger.debug("Client Connected [%s:%s]" % self.client_address) self.running = True self.framer = self.server.framer(self.server.decoder) self.server.threads.append(self) def finish(self): - ''' Callback for when a client disconnects - ''' + """ Callback for when a client disconnects + """ _logger.debug("Client Disconnected [%s:%s]" % self.client_address) self.server.threads.remove(self) def execute(self, request): - ''' The callback to call with the resulting message + """ The callback to call with the resulting message :param request: The decoded request message - ''' + """ try: context = self.server.context[request.unit_id] response = request.execute(context) @@ -70,45 +73,50 @@ def execute(self, request): response.unit_id = request.unit_id self.send(response) - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # Base class implementations - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def handle(self): - ''' Callback when we receive any data - ''' + """ Callback when we receive any data + """ raise NotImplementedException("Method not implemented by derived class") def send(self, message): - ''' Send a request (string) to the network + """ Send a request (string) to the network :param message: The unencoded modbus response - ''' + """ raise NotImplementedException("Method not implemented by derived class") class ModbusSingleRequestHandler(ModbusBaseRequestHandler): - ''' Implements the modbus server protocol + """ Implements the modbus server protocol This uses the socketserver.BaseRequestHandler to implement the client handler for a single client(serial clients) - ''' - + """ def handle(self): - ''' Callback when we receive any data - ''' + """ Callback when we receive any data + """ while self.running: try: data = self.request.recv(1024) if data: if _logger.isEnabledFor(logging.DEBUG): - _logger.debug("recv: " + " ".join([hex(byte2int(x)) for x in data])) + _logger.debug("recv: " + hexlify_packets(data)) + if isinstance(self.framer, ModbusAsciiFramer): unit_address = int(data[1:3], 16) elif isinstance(self.framer, ModbusBinaryFramer): unit_address = byte2int(data[1]) - else: + elif isinstance(self.framer, ModbusRtuFramer): unit_address = byte2int(data[0]) - + elif isinstance(self.framer, ModbusSocketFramer): + unit_address = byte2int(data[6]) + else: + _logger.error("Unknown" + " framer - {}".format(type(self.framer))) + unit_address = -1 if unit_address in self.server.context: self.framer.processIncomingPacket(data, self.execute) except Exception as msg: @@ -118,12 +126,12 @@ def handle(self): _logger.error("Socket error occurred %s" % msg) def send(self, message): - ''' Send a request (string) to the network + """ Send a request (string) to the network :param message: The unencoded modbus response - ''' + """ if message.should_respond: - #self.server.control.Counter.BusMessage += 1 + # self.server.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) if _logger.isEnabledFor(logging.DEBUG): _logger.debug('send: %s' % b2a_hex(pdu)) @@ -141,32 +149,36 @@ def __init__(self, request, client_address, server): class ModbusConnectedRequestHandler(ModbusBaseRequestHandler): - ''' Implements the modbus server protocol + """ Implements the modbus server protocol This uses the socketserver.BaseRequestHandler to implement the client handler for a connected protocol (TCP). - ''' + """ def handle(self): - '''Callback when we receive any data, until self.running becomes not True. Blocks indefinitely - awaiting data. If shutdown is required, then the global socket.settimeout() may be - used, to allow timely checking of self.running. However, since this also affects socket - connects, if there are outgoing socket connections used in the same program, then these will - be prevented, if the specfied timeout is too short. Hence, this is unreliable. - - To respond to Modbus...Server.server_close() (which clears each handler's self.running), - derive from this class to provide an alternative handler that awakens from time to time when - no input is available and checks self.running. Use Modbus...Server( handler=... ) keyword - to supply the alternative request handler class. - - ''' + """Callback when we receive any data, until self.running becomes False. + Blocks indefinitely awaiting data. If shutdown is required, then the + global socket.settimeout() may be used, to allow timely + checking of self.running. However, since this also affects socket + connects, if there are outgoing socket connections used in the same + program, then these will be prevented, if the specfied timeout is too + short. Hence, this is unreliable. + + To respond to Modbus...Server.server_close() (which clears each + handler's self.running), derive from this class to provide an + alternative handler that awakens from time to time when no input is + available and checks self.running. + Use Modbus...Server( handler=... ) keyword to supply the alternative + request handler class. + + """ reset_frame = False while self.running: try: data = self.request.recv(1024) if not data: self.running = False if _logger.isEnabledFor(logging.DEBUG): - _logger.debug(' '.join([hex(byte2int(x)) for x in data])) + _logger.debug('Handling data: ' + hexlify_packets(data)) # if not self.server.control.ListenOnly: self.framer.processIncomingPacket(data, self.execute) except socket.timeout as msg: @@ -186,12 +198,12 @@ def handle(self): reset_frame = False def send(self, message): - ''' Send a request (string) to the network + """ Send a request (string) to the network :param message: The unencoded modbus response - ''' + """ if message.should_respond: - #self.server.control.Counter.BusMessage += 1 + # self.server.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) if _logger.isEnabledFor(logging.DEBUG): _logger.debug('send: %s' % b2a_hex(pdu)) @@ -199,18 +211,18 @@ def send(self, message): class ModbusDisconnectedRequestHandler(ModbusBaseRequestHandler): - ''' Implements the modbus server protocol + """ Implements the modbus server protocol This uses the socketserver.BaseRequestHandler to implement the client handler for a disconnected protocol (UDP). The only difference is that we have to specify who to send the resulting packet data to. - ''' + """ socket = None def handle(self): - ''' Callback when we receive any data - ''' + """ Callback when we receive any data + """ reset_frame = False while self.running: try: @@ -218,7 +230,7 @@ def handle(self): if not data: self.running = False if _logger.isEnabledFor(logging.DEBUG): - _logger.debug(' '.join([hex(byte2int(x)) for x in data])) + _logger.debug('Handling data: ' + hexlify_packets(data)) # if not self.server.control.ListenOnly: self.framer.processIncomingPacket(data, self.execute) except socket.timeout: pass @@ -236,10 +248,10 @@ def handle(self): reset_frame = False def send(self, message): - ''' Send a request (string) to the network + """ Send a request (string) to the network :param message: The unencoded modbus response - ''' + """ if message.should_respond: #self.server.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) @@ -248,20 +260,21 @@ def send(self, message): return self.socket.sendto(pdu, self.client_address) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Server Implementations -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusTcpServer(socketserver.ThreadingTCPServer): - ''' + """ A modbus threaded tcp socket server We inherit and overload the socket server so that we can control the client threads as well as have a single server context instance. - ''' + """ - def __init__(self, context, framer=None, identity=None, address=None, handler=None, **kwargs): - ''' Overloaded initializer for the socket server + def __init__(self, context, framer=None, identity=None, + address=None, handler=None, **kwargs): + """ Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock uses its own empty structure. @@ -270,17 +283,20 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No :param framer: The framer strategy to use :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. - :param handler: A handler for each client session; default is ModbusConnectedRequestHandler - :param ignore_missing_slaves: True to not send errors on a request to a missing slave - ''' + :param handler: A handler for each client session; default is + ModbusConnectedRequestHandler + :param ignore_missing_slaves: True to not send errors on a request + to a missing slave + """ self.threads = [] self.decoder = ServerDecoder() - self.framer = framer or ModbusSocketFramer + self.framer = framer or ModbusSocketFramer self.context = context or ModbusServerContext() self.control = ModbusControlBlock() self.address = address or ("", Defaults.Port) self.handler = handler or ModbusConnectedRequestHandler - self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) + self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', + Defaults.IgnoreMissingSlaves) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) @@ -289,26 +305,26 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No self.address, self.handler) def process_request(self, request, client): - ''' Callback for connecting a new client thread + """ Callback for connecting a new client thread :param request: The request to handle :param client: The address of the client - ''' + """ _logger.debug("Started thread to serve client at " + str(client)) socketserver.ThreadingTCPServer.process_request(self, request, client) def shutdown(self): - ''' Stops the serve_forever loop. + """ Stops the serve_forever loop. Overridden to signal handlers to stop. - ''' + """ for thread in self.threads: thread.running = False socketserver.ThreadingTCPServer.shutdown(self) def server_close(self): - ''' Callback for stopping the running server - ''' + """ Callback for stopping the running server + """ _logger.debug("Modbus server stopped") self.socket.close() for thread in self.threads: @@ -316,16 +332,17 @@ def server_close(self): class ModbusUdpServer(socketserver.ThreadingUDPServer): - ''' + """ A modbus threaded udp socket server We inherit and overload the socket server so that we can control the client threads as well as have a single server context instance. - ''' + """ - def __init__(self, context, framer=None, identity=None, address=None, handler=None, **kwargs): - ''' Overloaded initializer for the socket server + def __init__(self, context, framer=None, identity=None, address=None, + handler=None, **kwargs): + """ Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock uses its own empty structure. @@ -334,17 +351,20 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No :param framer: The framer strategy to use :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. - :param handler: A handler for each client session; default is ModbusDisonnectedRequestHandler - :param ignore_missing_slaves: True to not send errors on a request to a missing slave - ''' + :param handler: A handler for each client session; default is + ModbusDisonnectedRequestHandler + :param ignore_missing_slaves: True to not send errors on a request + to a missing slave + """ self.threads = [] self.decoder = ServerDecoder() - self.framer = framer or ModbusSocketFramer + self.framer = framer or ModbusSocketFramer self.context = context or ModbusServerContext() self.control = ModbusControlBlock() self.address = address or ("", Defaults.Port) self.handler = handler or ModbusDisconnectedRequestHandler - self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) + self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', + Defaults.IgnoreMissingSlaves) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) @@ -353,18 +373,18 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No self.address, self.handler) def process_request(self, request, client): - ''' Callback for connecting a new client thread + """ Callback for connecting a new client thread :param request: The request to handle :param client: The address of the client - ''' + """ packet, socket = request # TODO I might have to rewrite _logger.debug("Started thread to serve client at " + str(client)) socketserver.ThreadingUDPServer.process_request(self, request, client) def server_close(self): - ''' Callback for stopping the running server - ''' + """ Callback for stopping the running server + """ _logger.debug("Modbus server stopped") self.socket.close() for thread in self.threads: @@ -372,18 +392,18 @@ def server_close(self): class ModbusSerialServer(object): - ''' + """ A modbus threaded serial socket server We inherit and overload the socket server so that we can control the client threads as well as have a single server context instance. - ''' + """ handler = None def __init__(self, context, framer=None, identity=None, **kwargs): - ''' Overloaded initializer for the socket server + """ Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock uses its own empty structure. @@ -397,49 +417,54 @@ def __init__(self, context, framer=None, identity=None, **kwargs): :param parity: Which kind of parity to use :param baudrate: The baud rate to use for the serial device :param timeout: The timeout to use for the serial device - :param ignore_missing_slaves: True to not send errors on a request to a missing slave - ''' + :param ignore_missing_slaves: True to not send errors on a request + to a missing slave + """ self.threads = [] self.decoder = ServerDecoder() - self.framer = framer or ModbusAsciiFramer + self.framer = framer or ModbusAsciiFramer self.context = context or ModbusServerContext() self.control = ModbusControlBlock() if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - self.device = kwargs.get('port', 0) + self.device = 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.parity = kwargs.get('parity', Defaults.Parity) self.baudrate = kwargs.get('baudrate', Defaults.Baudrate) - self.timeout = kwargs.get('timeout', Defaults.Timeout) - self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) - self.socket = None + self.timeout = kwargs.get('timeout', Defaults.Timeout) + self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', + Defaults.IgnoreMissingSlaves) + self.socket = None if self._connect(): self.is_running = True self._build_handler() def _connect(self): - ''' Connect to the serial server + """ Connect to the serial server :returns: True if connection succeeded, False otherwise - ''' + """ if self.socket: return True try: - self.socket = serial.Serial(port=self.device, timeout=self.timeout, - bytesize=self.bytesize, stopbits=self.stopbits, - baudrate=self.baudrate, parity=self.parity) + self.socket = serial.Serial(port=self.device, + timeout=self.timeout, + bytesize=self.bytesize, + stopbits=self.stopbits, + baudrate=self.baudrate, + parity=self.parity) except serial.SerialException as msg: _logger.error(msg) - return self.socket != None + return self.socket is not None def _build_handler(self): - ''' A helper method to create and monkeypatch + """ A helper method to create and monkeypatch a serial handler. :returns: A patched handler - ''' + """ request = self.socket request.send = request.write @@ -449,11 +474,11 @@ def _build_handler(self): self) def serve_forever(self): - ''' Callback for connecting a new client thread + """ Callback for connecting a new client thread :param request: The request to handle :param client: The address of the client - ''' + """ if self._connect(): _logger.debug("Started thread to serve client") if not self.handler: @@ -461,11 +486,12 @@ def serve_forever(self): while self.is_running: self.handler.handle() else: - _logger.error("Error opening serial port , Unable to start server!!") + _logger.error("Error opening serial port , " + "Unable to start server!!") def server_close(self): - ''' Callback for stopping the running server - ''' + """ Callback for stopping the running server + """ _logger.debug("Modbus server stopped") self.is_running = False self.handler.finish() @@ -474,38 +500,39 @@ def server_close(self): self.socket.close() -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Creation Factories -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # def StartTcpServer(context=None, identity=None, address=None, **kwargs): - ''' A factory to start and run a tcp modbus server + """ A factory to start and run a tcp modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. :param ignore_missing_slaves: True to not send errors on a request to a missing slave - ''' + """ framer = ModbusSocketFramer server = ModbusTcpServer(context, framer, identity, address, **kwargs) server.serve_forever() def StartUdpServer(context=None, identity=None, address=None, **kwargs): - ''' A factory to start and run a udp modbus server + """ A factory to start and run a udp modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. :param framer: The framer to operate with (default ModbusSocketFramer) - :param ignore_missing_slaves: True to not send errors on a request to a missing slave - ''' + :param ignore_missing_slaves: True to not send errors on a request + to a missing slave + """ framer = kwargs.pop('framer', ModbusSocketFramer) server = ModbusUdpServer(context, framer, identity, address, **kwargs) server.serve_forever() def StartSerialServer(context=None, identity=None, **kwargs): - ''' A factory to start and run a serial modbus server + """ A factory to start and run a serial modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure @@ -516,15 +543,18 @@ def StartSerialServer(context=None, identity=None, **kwargs): :param parity: Which kind of parity to use :param baudrate: The baud rate to use for the serial device :param timeout: The timeout to use for the serial device - :param ignore_missing_slaves: True to not send errors on a request to a missing slave - ''' + :param ignore_missing_slaves: True to not send errors on a request + to a missing slave + """ framer = kwargs.pop('framer', ModbusAsciiFramer) server = ModbusSerialServer(context, framer, identity, **kwargs) server.serve_forever() -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Exported symbols -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # + __all__ = [ "StartTcpServer", "StartUdpServer", "StartSerialServer" ] + diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 5ee20688e..27f3062f7 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -1,18 +1,20 @@ ''' Collection of transaction based abstractions ''' -import sys import struct import socket from binascii import b2a_hex, a2b_hex from serial import SerialException +from threading import RLock from pymodbus.exceptions import ModbusIOException, NotImplementedException from pymodbus.exceptions import InvalidMessageRecievedException -from pymodbus.constants import Defaults +from pymodbus.constants import Defaults from pymodbus.interfaces import IModbusFramer -from pymodbus.utilities import checkCRC, computeCRC -from pymodbus.utilities import checkLRC, computeLRC +from pymodbus.utilities import checkCRC, computeCRC +from pymodbus.utilities import checkLRC, computeLRC +from pymodbus.utilities import hexlify_packets from pymodbus.compat import iterkeys, imap, byte2int +from pymodbus.client.common import ModbusTransactionState # Python 2 compatibility. try: @@ -56,7 +58,9 @@ def __init__(self, client, **kwargs): self.tid = Defaults.TransactionId self.client = client self.retry_on_empty = kwargs.get('retry_on_empty', Defaults.RetryOnEmpty) - self.retries = kwargs.get('retries', Defaults.Retries) + self.retries = kwargs.get('retries', Defaults.Retries) or 1 + self._transaction_lock = RLock() + self._no_response_devices = [] if client: self._set_adu_size() @@ -111,95 +115,137 @@ def execute(self, request): ''' Starts the producer to send the next request to consumer.write(Frame(request)) ''' - retries = self.retries - request.transaction_id = self.getNextTID() - _logger.debug("Running transaction %d" % request.transaction_id) - self.client.framer.resetFrame() - expected_response_length = None - if not isinstance(self.client.framer, ModbusSocketFramer): - if hasattr(request, "get_response_pdu_size"): - response_pdu_size = request.get_response_pdu_size() - if isinstance(self.client.framer, ModbusAsciiFramer): - response_pdu_size = response_pdu_size * 2 - if response_pdu_size: - expected_response_length = self._calculate_response_length(response_pdu_size) - - while retries > 0: - try: - last_exception = None - self.client.connect() - packet = self.client.framer.buildPacket(request) - if _logger.isEnabledFor(logging.DEBUG): - _logger.debug("send: " + " ".join([hex(byte2int(x)) for x in packet])) - self._send(packet) - # exception = False - result = self._recv(expected_response_length or 1024) - - if not result and self.retry_on_empty: - retries -= 1 - continue - if _logger.isEnabledFor(logging.DEBUG): - _logger.debug("recv: " + " ".join([hex(byte2int(x)) for x in result])) - self.client.framer.processIncomingPacket(result, self.addTransaction) - break - except (socket.error, ModbusIOException, InvalidMessageRecievedException) as msg: - self.client.close() - _logger.debug("Transaction failed. (%s) " % msg) - retries -= 1 - last_exception = msg - response = self.getTransaction(request.transaction_id) - if not response: - if len(self.transactions): - response = self.getTransaction(tid=0) + with self._transaction_lock: + retries = self.retries + request.transaction_id = self.getNextTID() + _logger.debug("Running transaction %d" % request.transaction_id) + _buffer = hexlify_packets(self.client.framer._buffer) + _logger.debug("Current Frame : - {}".format(_buffer)) + expected_response_length = None + if not isinstance(self.client.framer, ModbusSocketFramer): + if hasattr(request, "get_response_pdu_size"): + response_pdu_size = request.get_response_pdu_size() + if isinstance(self.client.framer, ModbusAsciiFramer): + response_pdu_size = response_pdu_size * 2 + if response_pdu_size: + expected_response_length = self._calculate_response_length(response_pdu_size) + if request.unit_id in self._no_response_devices: + full = True else: - last_exception = last_exception or ("No Response " - "received from the remote unit") - response = ModbusIOException(last_exception) - - return response + full = False + response, last_exception = self._transact(request, + expected_response_length, + full=full + ) + if not response and ( + request.unit_id not in self._no_response_devices): + self._no_response_devices.append(request.unit_id) + elif request.unit_id in self._no_response_devices: + self._no_response_devices.remove(request.unit_id) + if not response and self.retry_on_empty and retries: + while retries > 0: + if hasattr(self.client, "state"): + _logger.debug("RESETTING Transaction state to " + "'IDLE' for retry") + self.client.state = ModbusTransactionState.IDLE + _logger.debug("Retry on empty - {}".format(retries)) + response, last_exception = self._transact( + request, + expected_response_length + ) + if not response: + retries -= 1 + continue + break + self.client.framer.processIncomingPacket(response, + self.addTransaction) + response = self.getTransaction(request.transaction_id) + if not response: + if len(self.transactions): + response = self.getTransaction(tid=0) + else: + last_exception = last_exception or ("No Response received " + "from the remote unit") + response = ModbusIOException(last_exception) + if hasattr(self.client, "state"): + _logger.debug("Changing transaction state from " + "'PROCESSING REPLY' to 'TRANSCATION_COMPLETE'") + self.client.state = ModbusTransactionState.TRANSCATION_COMPLETE + return response + + def _transact(self, packet, response_length, full=False): + """ + Does a Write and Read transaction + :param packet: packet to be sent + :param response_length: Expected response length + :param full: the target device was notorious for its no response. Dont + waste time this time by partial querying + :return: response + """ + last_exception = None + try: + self.client.connect() + packet = self.client.framer.buildPacket(packet) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug("send: " + hexlify_packets(packet)) + self._send(packet) + result = self._recv(response_length or 1024, full) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug("recv: " + hexlify_packets(result)) + except (socket.error, ModbusIOException, + InvalidMessageRecievedException) as msg: + self.client.close() + _logger.debug("Transaction failed. (%s) " % msg) + last_exception = msg + result = b'' + return result, last_exception def _send(self, packet): return self.client._send(packet) - def _recv(self, expected_response_length): - retries = self.retries - exception = False - while retries: - result = self.client._recv(expected_response_length or 1024) - while result and expected_response_length and len( - result) < expected_response_length: - if not exception and not self._check_response(result): - exception = True - expected_response_length = self._calculate_exception_length() - continue + def _recv(self, expected_response_length, full): + # retries = self.retries + # exception = False + expected_response_length = expected_response_length or 1024 + if not full: + exception_length = self._calculate_exception_length() + if isinstance(self.client.framer, ModbusSocketFramer): + min_size = 8 + elif isinstance(self.client.framer, ModbusRtuFramer): + min_size = 2 + elif isinstance(self.client.framer, ModbusAsciiFramer): + min_size = 5 + elif isinstance(self.client.framer, ModbusBinaryFramer): + min_size = 3 + else: + min_size = expected_response_length + + read_min = self.client._recv(min_size) + if read_min: if isinstance(self.client.framer, ModbusSocketFramer): - # Ommit UID, which is included in header size - h_size = self.client.framer._hsize - length = struct.unpack(">H", result[4:6])[0] -1 - expected_response_length = h_size + length - - if expected_response_length != len(result): - _logger.debug("Expected - {} bytes, " - "Actual - {} bytes".format( - expected_response_length, len(result)) - ) - try: - r = self.client._recv( - expected_response_length - len(result) - ) - result += r - if not r: - # If no response being recived there - # is no point in conitnuing - break - except (TimeoutError, socket.timeout, SerialException): - break + func_code = byte2int(read_min[-1]) + elif isinstance(self.client.framer, ModbusRtuFramer): + func_code = byte2int(read_min[-1]) + elif isinstance(self.client.framer, ModbusAsciiFramer): + func_code = int(read_min[3:5], 16) + elif isinstance(self.client.framer, ModbusBinaryFramer): + func_code = byte2int(read_min[-1]) else: - break - - if result: - break - retries -= 1 + func_code = -1 + + if func_code < 0x80: # Not an error + if isinstance(self.client.framer, ModbusSocketFramer): + # Ommit UID, which is included in header size + h_size = self.client.framer._hsize + length = struct.unpack(">H", read_min[4:6])[0] - 1 + expected_response_length = h_size + length + expected_response_length -= min_size + else: + expected_response_length = exception_length - min_size + else: + read_min = b'' + result = self.client._recv(expected_response_length) + result = read_min + result return result def addTransaction(self, request, tid=None): @@ -466,7 +512,7 @@ def processIncomingPacket(self, data, callback): :param data: The new packet data :param callback: The function to send results to ''' - _logger.debug(' '.join([hex(byte2int(x)) for x in data])) + _logger.debug("Processing: "+ hexlify_packets(data)) self.addToFrame(data) while True: if self.isFrameReady(): @@ -605,6 +651,7 @@ def advanceFrame(self): except KeyError: # Error response, no header len found self.resetFrame() + _logger.debug("Frame advanced, resetting header!!") self._header = {} def resetFrame(self): @@ -615,6 +662,8 @@ def resetFrame(self): end of the message (python just doesn't have the resolution to check for millisecond delays). ''' + _logger.debug("Resetting frame - Current Frame in " + "buffer - {}".format(hexlify_packets(self._buffer))) self._buffer = b'' self._header = {} @@ -658,10 +707,12 @@ def getFrame(self): :returns: The frame data or '' ''' - start = self._hsize - end = self._header['len'] - 2 + start = self._hsize + end = self._header['len'] - 2 buffer = self._buffer[start:end] - if end > 0: return buffer + if end > 0: + _logger.debug("Getting Frame - {}".format(hexlify_packets(buffer))) + return buffer return '' def populateResult(self, result): @@ -740,6 +791,7 @@ def getRawFrame(self): """ Returns the complete buffer """ + _logger.debug("Getting Raw Frame - {}".format(self._buffer)) return self._buffer @@ -966,7 +1018,7 @@ def checkFrame(self): end = self._buffer.find(self._end) if (end != -1): self._header['len'] = end - self._header['uid'] = struct.unpack('>B', self._buffer[1:2]) + self._header['uid'] = struct.unpack('>B', self._buffer[1:2])[0] self._header['crc'] = struct.unpack('>H', self._buffer[end - 2:end])[0] data = self._buffer[start + 1:end - 2] return checkCRC(data, self._header['crc']) diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index 20ef72663..6205829da 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -205,6 +205,16 @@ def rtuFrameSize(data, byte_count_pos): """ return byte2int(data[byte_count_pos]) + byte_count_pos + 3 + +def hexlify_packets(packet): + """ + Returns hex representation of bytestring recieved + :param packet: + :return: + """ + if not packet: + return '' + return " ".join([hex(byte2int(x)) for x in packet]) # --------------------------------------------------------------------------- # # Exported symbols # --------------------------------------------------------------------------- # diff --git a/pymodbus/version.py b/pymodbus/version.py index 30440f39a..776d3d31c 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', 1, 4, 0) +version = Version('pymodbus', 1, 5, 0) version.__name__ = 'pymodbus' # fix epydoc error diff --git a/test/test_client_sync.py b/test/test_client_sync.py index 516b44298..beda6049e 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -220,7 +220,9 @@ def testBasicSyncSerialClient(self, mock_serial): mock_serial.read = lambda size: '\x00' * size client = ModbusSerialClient() client.socket = mock_serial + client.state = 0 self.assertEqual(0, client._send(None)) + client.state = 0 self.assertEqual(1, client._send('\x00')) self.assertEqual('\x00', client._recv(1)) @@ -255,7 +257,9 @@ def testSerialClientSend(self, mock_serial): self.assertRaises(ConnectionException, lambda: client._send(None)) # client.connect() client.socket = mock_serial + client.state = 0 self.assertEqual(0, client._send(None)) + client.state = 0 self.assertEqual(4, client._send('1234')) @patch("serial.Serial") @@ -268,7 +272,9 @@ def testSerialClientCleanupBufferBeforeSend(self, mock_serial): self.assertRaises(ConnectionException, lambda: client._send(None)) # client.connect() client.socket = mock_serial + client.state = 0 self.assertEqual(0, client._send(None)) + client.state = 0 self.assertEqual(4, client._send('1234')) def testSerialClientRecv(self): diff --git a/test/test_server_async.py b/test/test_server_async.py index 8fbd9758f..0ed38b5d2 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -78,7 +78,7 @@ def testDataReceived(self): mock_data = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" protocol.factory = MagicMock() protocol.factory.control.ListenOnly = False - protocol.factory.store = [byte2int(mock_data[0])] + protocol.factory.store = [byte2int(mock_data[6])] protocol.framer = protocol._execute = MagicMock() protocol.dataReceived(mock_data) @@ -100,7 +100,10 @@ def testTcpExecuteSuccess(self): def testTcpExecuteFailure(self): protocol = ModbusTcpProtocol() + protocol.factory = MagicMock() + protocol.factory.store = MagicMock() protocol.store = MagicMock() + protocol.factory.ignore_missing_slaves = False request = MagicMock() protocol._send = MagicMock() diff --git a/test/test_server_sync.py b/test/test_server_sync.py index d9351bd07..fbb1e2df5 100644 --- a/test/test_server_sync.py +++ b/test/test_server_sync.py @@ -113,7 +113,8 @@ def _callback1(a, b): handler.framer.processIncomingPacket.side_effect = _callback1 handler.running = True # Ugly hack - handler.server.context = ModbusServerContext(slaves={18: None}, single=False) + handler.server.context = ModbusServerContext(slaves={-1: None}, + single=False) handler.handle() self.assertEqual(handler.framer.processIncomingPacket.call_count, 1) @@ -181,7 +182,7 @@ def _callback(a, b): handler.request.recv.return_value = None handler.running = True handler.handle() - self.assertEqual(handler.framer.processIncomingPacket.call_count, 3) + self.assertEqual(handler.framer.processIncomingPacket.call_count, 4) #-----------------------------------------------------------------------# # Test Disconnected Request Handler @@ -239,7 +240,7 @@ def _callback(a, b): handler.request = (None, handler.request) handler.running = True handler.handle() - self.assertEqual(handler.framer.processIncomingPacket.call_count, 3) + self.assertEqual(handler.framer.processIncomingPacket.call_count, 4) #-----------------------------------------------------------------------# # Test TCP Server From 689708a667ca665746ef8c9741bdc0c0e5a6170b Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Fri, 26 Jan 2018 18:17:34 +0530 Subject: [PATCH 2/5] Fix #221 timing enhancements, #188 workarounds --- examples/contrib/message_parser.py | 8 -- pymodbus/client/sync.py | 45 +++++---- pymodbus/transaction.py | 144 ++++++++++++++++------------- test/test_client_sync.py | 4 +- 4 files changed, 109 insertions(+), 92 deletions(-) diff --git a/examples/contrib/message_parser.py b/examples/contrib/message_parser.py index 5b5602c41..f44e36c46 100755 --- a/examples/contrib/message_parser.py +++ b/examples/contrib/message_parser.py @@ -144,14 +144,6 @@ def get_options(): help="If the incoming message is in hexadecimal format", action="store_true", dest="transaction", default=False) - parser.add_option("-t", "--transaction", - help="If the incoming message is in hexadecimal format", - action="store_true", dest="transaction", default=False) - - parser.add_option("-t", "--transaction", - help="If the incoming message is in hexadecimal format", - action="store_true", dest="transaction", default=False) - (opt, arg) = parser.parse_args() if not opt.message and len(arg) > 0: diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index fb214297a..554429187 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -324,11 +324,12 @@ def __init__(self, method='ascii', **kwargs): self.baudrate = kwargs.get('baudrate', Defaults.Baudrate) self.timeout = kwargs.get('timeout', Defaults.Timeout) if self.method == "rtu": - self._last_frame_end = 0.0 + self._last_frame_end = None if self.baudrate > 19200: self._silent_interval = 1.75/1000 # ms else: self._silent_interval = 3.5 * (1 + 8 + 2) / self.baudrate + self._silent_interval = round(self._silent_interval, 6) @staticmethod def __implementation(method): @@ -353,7 +354,8 @@ def connect(self): :returns: True if connection succeeded, False otherwise """ - if self.socket: return True + if self.socket: + return True try: self.socket = serial.Serial(port=self.port, timeout=self.timeout, @@ -390,26 +392,30 @@ def _send(self, request): "state - {}".format(TransactionStateString[self.state])) while self.state != ModbusTransactionState.IDLE: if self.state == ModbusTransactionState.TRANSCATION_COMPLETE: + ts = round(time.time(), 6) + _logger.debug("Changing state to IDLE - Last Frame End - {}, " + "Current Time stamp - {}".format( + self._last_frame_end, ts)) + if self.method == "rtu": + if self._last_frame_end: + idle_time = self._last_frame_end + self._silent_interval + if round(ts-idle_time, 6) <= self._silent_interval: + _logger.debug("Waiting for 3.5 char before next " + "send - {} ms".format( + self._silent_interval*1000)) + time.sleep(self._silent_interval) + else: + # Recovering from last error ?? + time.sleep(self._silent_interval) self.state = ModbusTransactionState.IDLE else: + _logger.debug("Sleeping") time.sleep(self._silent_interval) _logger.debug("Transaction state 'IDLE', intiating a new transaction") self.state = ModbusTransactionState.SENDING if not self.socket: raise ConnectionException(self.__str__()) if request: - ts = time.time() - if self.method == "rtu": - if self._last_frame_end: - if ts < self._last_frame_end + self._silent_interval: - _logger.debug("waiting for 3.5 char before next " - "send".format(self._silent_interval)) - time.sleep( - self._last_frame_end + self._silent_interval - ts - ) - _logger.debug("waited for - {} " - "ms".format(self._silent_interval*1000)) - try: in_waiting = ("in_waiting" if hasattr( self.socket, "in_waiting") else "inWaiting") @@ -431,7 +437,7 @@ def _send(self, request): "to 'WAITING FOR REPLY'") self.state = ModbusTransactionState.WAITING_FOR_REPLY if self.method == "rtu": - self._last_frame_end = time.time() + self._last_frame_end = round(time.time(), 6) return size return 0 @@ -444,11 +450,12 @@ def _recv(self, size): if not self.socket: raise ConnectionException(self.__str__()) result = self.socket.read(size) - _logger.debug("Changing transaction state from 'WAITING FOR REPLY' " - "to 'PROCESSING REPLY'") - self.state = ModbusTransactionState.PROCESSING_REPLY + if self.state != ModbusTransactionState.PROCESSING_REPLY: + _logger.debug("Changing transaction state from " + "'WAITING FOR REPLY' to 'PROCESSING REPLY'") + self.state = ModbusTransactionState.PROCESSING_REPLY if self.method == "rtu": - self._last_frame_end = time.time() + self._last_frame_end = round(time.time(), 6) return result def __str__(self): diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 27f3062f7..b7eb6b350 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -116,62 +116,70 @@ def execute(self, request): consumer.write(Frame(request)) ''' with self._transaction_lock: - retries = self.retries - request.transaction_id = self.getNextTID() - _logger.debug("Running transaction %d" % request.transaction_id) - _buffer = hexlify_packets(self.client.framer._buffer) - _logger.debug("Current Frame : - {}".format(_buffer)) - expected_response_length = None - if not isinstance(self.client.framer, ModbusSocketFramer): - if hasattr(request, "get_response_pdu_size"): - response_pdu_size = request.get_response_pdu_size() - if isinstance(self.client.framer, ModbusAsciiFramer): - response_pdu_size = response_pdu_size * 2 - if response_pdu_size: - expected_response_length = self._calculate_response_length(response_pdu_size) - if request.unit_id in self._no_response_devices: - full = True - else: - full = False - response, last_exception = self._transact(request, - expected_response_length, - full=full - ) - if not response and ( - request.unit_id not in self._no_response_devices): - self._no_response_devices.append(request.unit_id) - elif request.unit_id in self._no_response_devices: - self._no_response_devices.remove(request.unit_id) - if not response and self.retry_on_empty and retries: - while retries > 0: - if hasattr(self.client, "state"): - _logger.debug("RESETTING Transaction state to " - "'IDLE' for retry") - self.client.state = ModbusTransactionState.IDLE - _logger.debug("Retry on empty - {}".format(retries)) - response, last_exception = self._transact( - request, - expected_response_length - ) - if not response: - retries -= 1 - continue - break - self.client.framer.processIncomingPacket(response, - self.addTransaction) - response = self.getTransaction(request.transaction_id) - if not response: - if len(self.transactions): - response = self.getTransaction(tid=0) + try: + retries = self.retries + request.transaction_id = self.getNextTID() + _logger.debug("Running transaction %d" % request.transaction_id) + _buffer = hexlify_packets(self.client.framer._buffer) + if _buffer: + _logger.debug("Clearing current Frame : - {}".format(_buffer)) + self.client.framer.resetFrame() + + expected_response_length = None + if not isinstance(self.client.framer, ModbusSocketFramer): + if hasattr(request, "get_response_pdu_size"): + response_pdu_size = request.get_response_pdu_size() + if isinstance(self.client.framer, ModbusAsciiFramer): + response_pdu_size = response_pdu_size * 2 + if response_pdu_size: + expected_response_length = self._calculate_response_length(response_pdu_size) + if request.unit_id in self._no_response_devices: + full = True else: - last_exception = last_exception or ("No Response received " - "from the remote unit") - response = ModbusIOException(last_exception) - if hasattr(self.client, "state"): - _logger.debug("Changing transaction state from " - "'PROCESSING REPLY' to 'TRANSCATION_COMPLETE'") + full = False + response, last_exception = self._transact(request, + expected_response_length, + full=full + ) + if not response and ( + request.unit_id not in self._no_response_devices): + self._no_response_devices.append(request.unit_id) + elif request.unit_id in self._no_response_devices: + self._no_response_devices.remove(request.unit_id) + if not response and self.retry_on_empty and retries: + while retries > 0: + if hasattr(self.client, "state"): + _logger.debug("RESETTING Transaction state to " + "'IDLE' for retry") + self.client.state = ModbusTransactionState.IDLE + _logger.debug("Retry on empty - {}".format(retries)) + response, last_exception = self._transact( + request, + expected_response_length + ) + if not response: + retries -= 1 + continue + break + self.client.framer.processIncomingPacket(response, + self.addTransaction) + response = self.getTransaction(request.transaction_id) + if not response: + if len(self.transactions): + response = self.getTransaction(tid=0) + else: + last_exception = last_exception or ("No Response received " + "from the remote unit") + response = ModbusIOException(last_exception) + if hasattr(self.client, "state"): + _logger.debug("Changing transaction state from " + "'PROCESSING REPLY' to 'TRANSCATION_COMPLETE'") + self.client.state = ModbusTransactionState.TRANSCATION_COMPLETE + return response + except Exception as ex: + _logger.exception(ex) self.client.state = ModbusTransactionState.TRANSCATION_COMPLETE - return response + raise def _transact(self, packet, response_length, full=False): """ @@ -187,11 +195,11 @@ def _transact(self, packet, response_length, full=False): self.client.connect() packet = self.client.framer.buildPacket(packet) if _logger.isEnabledFor(logging.DEBUG): - _logger.debug("send: " + hexlify_packets(packet)) + _logger.debug("SEND: " + hexlify_packets(packet)) self._send(packet) result = self._recv(response_length or 1024, full) if _logger.isEnabledFor(logging.DEBUG): - _logger.debug("recv: " + hexlify_packets(result)) + _logger.debug("RECV: " + hexlify_packets(result)) except (socket.error, ModbusIOException, InvalidMessageRecievedException) as msg: self.client.close() @@ -240,12 +248,22 @@ def _recv(self, expected_response_length, full): length = struct.unpack(">H", read_min[4:6])[0] - 1 expected_response_length = h_size + length expected_response_length -= min_size + total = expected_response_length + min_size else: expected_response_length = exception_length - min_size + total = expected_response_length + min_size + else: + total = expected_response_length else: read_min = b'' + total = expected_response_length result = self.client._recv(expected_response_length) result = read_min + result + actual = len(result) + if actual != total: + _logger.debug("Incomplete message received, " + "Expected {} bytes Recieved " + "{} bytes !!!!".format(total, actual)) return result def addTransaction(self, request, tid=None): @@ -322,7 +340,7 @@ def addTransaction(self, request, tid=None): :param tid: The overloaded transaction id to use ''' tid = tid if tid != None else request.transaction_id - _logger.debug("adding transaction %d" % tid) + _logger.debug("Adding transaction %d" % tid) self.transactions[tid] = request def getTransaction(self, tid): @@ -332,7 +350,7 @@ def getTransaction(self, tid): :param tid: The transaction to retrieve ''' - _logger.debug("getting transaction %d" % tid) + _logger.debug("Getting transaction %d" % tid) return self.transactions.pop(tid, None) def delTransaction(self, tid): @@ -340,7 +358,7 @@ def delTransaction(self, tid): :param tid: The transaction to remove ''' - _logger.debug("deleting transaction %d" % tid) + _logger.debug("Deleting transaction %d" % tid) self.transactions.pop(tid, None) @@ -374,7 +392,7 @@ def addTransaction(self, request, tid=None): :param tid: The overloaded transaction id to use ''' tid = tid if tid != None else request.transaction_id - _logger.debug("adding transaction %d" % tid) + _logger.debug("Adding transaction %d" % tid) self.transactions.append(request) def getTransaction(self, tid): @@ -384,7 +402,7 @@ def getTransaction(self, tid): :param tid: The transaction to retrieve ''' - _logger.debug("getting transaction %s" % str(tid)) + _logger.debug("Getting transaction %s" % str(tid)) return self.transactions.pop(0) if self.transactions else None def delTransaction(self, tid): @@ -392,7 +410,7 @@ def delTransaction(self, tid): :param tid: The transaction to remove ''' - _logger.debug("deleting transaction %d" % tid) + _logger.debug("Deleting transaction %d" % tid) if self.transactions: self.transactions.pop(0) @@ -713,7 +731,7 @@ def getFrame(self): if end > 0: _logger.debug("Getting Frame - {}".format(hexlify_packets(buffer))) return buffer - return '' + return b'' def populateResult(self, result): ''' Populates the modbus result header diff --git a/test/test_client_sync.py b/test/test_client_sync.py index beda6049e..67a2fc5cb 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -204,9 +204,9 @@ def testSyncSerialClientInstantiation(self): def testSyncSerialRTUClientTimeouts(self): client = ModbusSerialClient(method="rtu", baudrate=9600) - assert client._silent_interval == (3.5 * 11/9600) + assert client._silent_interval == round((3.5 * 11/9600), 6) client = ModbusSerialClient(method="rtu", baudrate=38400) - assert client._silent_interval == (1.75/1000) + assert client._silent_interval == round((1.75/1000), 6) @patch("serial.Serial") From 900db55deea698933d7c294404a076adc2f21713 Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Mon, 9 Apr 2018 08:35:27 +0530 Subject: [PATCH 3/5] 1. #284 Async servers - Option to start reactor outside StartServer function 2. #283 Fix BinaryPayloadDecoder/Builder issues when using with pymodbus server 3. #278 Fix issue with sync/async servers failing to handle requests with transaction id > 255 4. #221 Move timing and transcational logic to framers for sync clients 5. #221 More debug logs for sync clients 6. Misc updates with examples and minor enhancements --- CHANGELOG.rst | 4 + doc/source/library/pymodbus.framer.rst | 47 ++ doc/source/library/pymodbus.rst | 2 + examples/common/asynchronous_client.py | 43 +- examples/common/asynchronous_processor.py | 10 +- examples/common/asynchronous_server.py | 45 +- examples/common/modbus_payload.py | 78 ++- examples/common/modbus_payload_server.py | 28 +- examples/common/synchronous_client.py | 18 +- examples/common/synchronous_client_ext.py | 51 +- examples/common/synchronous_server.py | 35 +- examples/contrib/message_parser.py | 17 +- pymodbus/client/async.py | 15 +- pymodbus/client/common.py | 16 +- pymodbus/client/sync.py | 141 ++-- pymodbus/datastore/context.py | 4 + pymodbus/framer/__init__.py | 47 ++ pymodbus/framer/ascii_framer.py | 214 ++++++ pymodbus/framer/binary_framer.py | 227 ++++++ pymodbus/framer/rtu_framer.py | 330 +++++++++ pymodbus/framer/socket_framer.py | 217 ++++++ pymodbus/payload.py | 133 ++-- pymodbus/server/async.py | 153 +++-- pymodbus/server/sync.py | 50 +- pymodbus/transaction.py | 799 +--------------------- pymodbus/utilities.py | 28 + requirements.txt | 2 +- test/test_client_sync.py | 4 +- test/test_payload.py | 2 +- test/test_server_async.py | 4 +- test/test_server_sync.py | 11 +- test/test_transaction.py | 26 +- 32 files changed, 1694 insertions(+), 1107 deletions(-) create mode 100644 doc/source/library/pymodbus.framer.rst create mode 100644 pymodbus/framer/__init__.py create mode 100644 pymodbus/framer/ascii_framer.py create mode 100644 pymodbus/framer/binary_framer.py create mode 100644 pymodbus/framer/rtu_framer.py create mode 100644 pymodbus/framer/socket_framer.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8822c5932..e13bfc559 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,10 @@ Version 1.5.0 * Fix tcp servers (sync/async) not processing requests with transaction id > 255 * Introduce new api to check if the received response is an error or not (response.isError()) +* Move timing logic to framers so that irrespective of client, correct timing logics are followed. +* Move framers from transaction.py to respective modules +* Fix modbus payload builder and decoder +* Async servers can now have an option to defer `reactor.run()` when using `StartServer(...,defer_reactor_run=True)` * Fix Misc examples Version 1.4.0 diff --git a/doc/source/library/pymodbus.framer.rst b/doc/source/library/pymodbus.framer.rst new file mode 100644 index 000000000..dd984c99b --- /dev/null +++ b/doc/source/library/pymodbus.framer.rst @@ -0,0 +1,47 @@ +pymodbus\.framer package +======================== + +Submodules +---------- + +pymodbus\.framer\.ascii_framer module +------------------------------------- + +.. automodule:: pymodbus.framer.ascii_framer + :members: + :undoc-members: + :show-inheritance: + +pymodbus\.framer\.binary_framer module +-------------------------------------- + +.. automodule:: pymodbus.framer.binary_framer + :members: + :undoc-members: + :show-inheritance: + +pymodbus\.framer\.rtu_framer module +----------------------------------- + +.. automodule:: pymodbus.framer.rtu_framer + :members: + :undoc-members: + :show-inheritance: + +pymodbus\.framer\.socket_framer module +-------------------------------------- + +.. automodule:: pymodbus.framer.socket_framer + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: pymodbus.framer + :members: + :undoc-members: + :show-inheritance: + diff --git a/doc/source/library/pymodbus.rst b/doc/source/library/pymodbus.rst index 9517ad686..76b9f0bd2 100644 --- a/doc/source/library/pymodbus.rst +++ b/doc/source/library/pymodbus.rst @@ -8,9 +8,11 @@ Subpackages pymodbus.client pymodbus.datastore + pymodbus.framer pymodbus.internal pymodbus.server + Submodules ---------- diff --git a/examples/common/asynchronous_client.py b/examples/common/asynchronous_client.py index ed77be262..584266efe 100755 --- a/examples/common/asynchronous_client.py +++ b/examples/common/asynchronous_client.py @@ -16,13 +16,16 @@ # choose the requested modbus protocol # --------------------------------------------------------------------------- # from pymodbus.client.async import ModbusClientProtocol -#from pymodbus.client.async import ModbusUdpClientProtocol +from pymodbus.client.async import ModbusUdpClientProtocol +from pymodbus.framer.rtu_framer import ModbusRtuFramer # --------------------------------------------------------------------------- # # configure the client logging # --------------------------------------------------------------------------- # import logging -logging.basicConfig() +FORMAT = ('%(asctime)-15s %(threadName)-15s' + ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') +logging.basicConfig(format=FORMAT) log = logging.getLogger() log.setLevel(logging.DEBUG) @@ -34,7 +37,6 @@ def dassert(deferred, callback): def _assertor(value): assert value - deferred.addCallback(lambda r: _assertor(callback(r))) deferred.addErrback(lambda _: _assertor(False)) @@ -47,8 +49,20 @@ def _assertor(value): # --------------------------------------------------------------------------- # +def processResponse(result): + log.debug(result) + + def exampleRequests(client): rr = client.read_coils(1, 1, unit=0x02) + rr.addCallback(processResponse) + rr = client.read_holding_registers(1, 1, unit=0x02) + rr.addCallback(processResponse) + rr = client.read_discrete_inputs(1, 1, unit=0x02) + rr.addCallback(processResponse) + rr = client.read_input_registers(1, 1, unit=0x02) + rr.addCallback(processResponse) + stopAsynchronousTest(client) # --------------------------------------------------------------------------- # # example requests @@ -61,7 +75,16 @@ def exampleRequests(client): # deferred assert helper(dassert). # --------------------------------------------------------------------------- # -UNIT = 0x01 + +UNIT = 0x00 + + +def stopAsynchronousTest(client): + # ----------------------------------------------------------------------- # + # close the client at some time later + # ----------------------------------------------------------------------- # + reactor.callLater(1, client.transport.loseConnection) + reactor.callLater(2, reactor.stop) def beginAsynchronousTest(client): rq = client.write_coil(1, True, unit=UNIT) @@ -99,12 +122,8 @@ def beginAsynchronousTest(client): rr = client.read_input_registers(1, 8, unit=UNIT) dassert(rq, lambda r: r.registers == [20]*8) # test the expected value dassert(rr, lambda r: r.registers == [17]*8) # test the expected value + stopAsynchronousTest(client) - # ----------------------------------------------------------------------- # - # close the client at some time later - # ----------------------------------------------------------------------- # - reactor.callLater(1, client.transport.loseConnection) - reactor.callLater(2, reactor.stop) # --------------------------------------------------------------------------- # # extra requests @@ -134,5 +153,11 @@ def beginAsynchronousTest(client): if __name__ == "__main__": defer = protocol.ClientCreator( reactor, ModbusClientProtocol).connectTCP("localhost", 5020) + + # TCP server with a different framer + + # defer = protocol.ClientCreator( + # reactor, ModbusClientProtocol, framer=ModbusRtuFramer).connectTCP( + # "localhost", 5020) defer.addCallback(beginAsynchronousTest) reactor.run() diff --git a/examples/common/asynchronous_processor.py b/examples/common/asynchronous_processor.py index 07dec9216..54bcdb7f8 100755 --- a/examples/common/asynchronous_processor.py +++ b/examples/common/asynchronous_processor.py @@ -26,14 +26,16 @@ # configure the client logging # --------------------------------------------------------------------------- # import logging -logging.basicConfig() -log = logging.getLogger("pymodbus") +FORMAT = ('%(asctime)-15s %(threadName)-15s' + ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') +logging.basicConfig(format=FORMAT) +log = logging.getLogger() log.setLevel(logging.DEBUG) # --------------------------------------------------------------------------- # # state a few constants # --------------------------------------------------------------------------- # -SERIAL_PORT = "/dev/ttyp0" +SERIAL_PORT = "/dev/ptyp0" STATUS_REGS = (1, 2) STATUS_COILS = (1, 3) CLIENT_DELAY = 1 @@ -173,7 +175,7 @@ def write(self, response): def main(): log.debug("Initializing the client") - framer = ModbusFramer(ClientDecoder()) + framer = ModbusFramer(ClientDecoder(), client=None) reader = LoggingLineReader() factory = ExampleFactory(framer, reader) SerialModbusClient(factory, SERIAL_PORT, reactor) diff --git a/examples/common/asynchronous_server.py b/examples/common/asynchronous_server.py index ea545a568..2186698cc 100755 --- a/examples/common/asynchronous_server.py +++ b/examples/common/asynchronous_server.py @@ -17,13 +17,17 @@ from pymodbus.device import ModbusDeviceIdentification from pymodbus.datastore import ModbusSequentialDataBlock from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext -from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer +from pymodbus.transaction import (ModbusRtuFramer, + ModbusAsciiFramer, + ModbusBinaryFramer) # --------------------------------------------------------------------------- # # configure the service logging # --------------------------------------------------------------------------- # import logging -logging.basicConfig() +FORMAT = ('%(asctime)-15s %(threadName)-15s' + ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') +logging.basicConfig(format=FORMAT) log = logging.getLogger() log.setLevel(logging.DEBUG) @@ -101,18 +105,41 @@ def run_async_server(): identity.VendorUrl = 'http://github.com/bashwork/pymodbus/' identity.ProductName = 'Pymodbus Server' identity.ModelName = 'Pymodbus Server' - identity.MajorMinorRevision = '1.0' + identity.MajorMinorRevision = '1.5' # ----------------------------------------------------------------------- # # run the server you want # ----------------------------------------------------------------------- # - + + # TCP Server + StartTcpServer(context, identity=identity, address=("localhost", 5020)) - # StartUdpServer(context, identity=identity, address=("localhost", 502)) - # StartSerialServer(context, identity=identity, - # port='/dev/pts/3', framer=ModbusRtuFramer) - # StartSerialServer(context, identity=identity, - # port='/dev/pts/3', framer=ModbusAsciiFramer) + + # TCP Server with deferred reactor run + + # from twisted.internet import reactor + # StartTcpServer(context, identity=identity, address=("localhost", 5020), + # defer_reactor_run=True) + # reactor.run() + + # Server with RTU framer + # StartTcpServer(context, identity=identity, address=("localhost", 5020), + # framer=ModbusRtuFramer) + + # UDP Server + # StartUdpServer(context, identity=identity, address=("127.0.0.1", 5020)) + + # RTU Server + # StartSerialServer(context, identity=identity, + # port='/dev/ttyp0', framer=ModbusRtuFramer) + + # ASCII Server + # StartSerialServer(context, identity=identity, + # port='/dev/ttyp0', framer=ModbusAsciiFramer) + + # Binary Server + # StartSerialServer(context, identity=identity, + # port='/dev/ttyp0', framer=ModbusBinaryFramer) if __name__ == "__main__": diff --git a/examples/common/modbus_payload.py b/examples/common/modbus_payload.py index a9d74cd0e..129db6ec7 100755 --- a/examples/common/modbus_payload.py +++ b/examples/common/modbus_payload.py @@ -10,21 +10,25 @@ from pymodbus.payload import BinaryPayloadBuilder from pymodbus.client.sync import ModbusTcpClient as ModbusClient from pymodbus.compat import iteritems +from collections import OrderedDict # --------------------------------------------------------------------------- # # configure the client logging # --------------------------------------------------------------------------- # + import logging -logging.basicConfig() +FORMAT = ('%(asctime)-15s %(threadName)-15s' + ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') +logging.basicConfig(format=FORMAT) log = logging.getLogger() -log.setLevel(logging.INFO) +log.setLevel(logging.DEBUG) def run_binary_payload_ex(): # ----------------------------------------------------------------------- # # We are going to use a simple client to send our requests # ----------------------------------------------------------------------- # - client = ModbusClient('127.0.0.1', port=5440) + client = ModbusClient('127.0.0.1', port=5020) client.connect() # ----------------------------------------------------------------------- # @@ -67,19 +71,36 @@ def run_binary_payload_ex(): # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # # ----------------------------------------------------------------------- # - builder = BinaryPayloadBuilder(byteorder=Endian.Little, - wordorder=Endian.Big) + builder = BinaryPayloadBuilder(byteorder=Endian.Big, + wordorder=Endian.Little) builder.add_string('abcdefgh') - builder.add_32bit_float(22.34) - builder.add_16bit_uint(0x1234) - builder.add_16bit_uint(0x5678) - builder.add_8bit_int(0x12) builder.add_bits([0, 1, 0, 1, 1, 0, 1, 0]) - builder.add_32bit_uint(0x12345678) + builder.add_8bit_int(-0x12) + builder.add_8bit_uint(0x12) + builder.add_16bit_int(-0x5678) + builder.add_16bit_uint(0x1234) builder.add_32bit_int(-0x1234) - builder.add_64bit_int(0x1234567890ABCDEF) + builder.add_32bit_uint(0x12345678) + builder.add_32bit_float(22.34) + builder.add_32bit_float(-22.34) + builder.add_64bit_int(-0xDEADBEEF) + builder.add_64bit_uint(0x12345678DEADBEEF) + builder.add_64bit_uint(0x12345678DEADBEEF) + builder.add_64bit_float(123.45) + builder.add_64bit_float(-123.45) + payload = builder.to_registers() + print("-" * 60) + print("Writing Registers") + print("-" * 60) + print(payload) + print("\n") payload = builder.build() address = 0 + # Can write registers + # registers = builder.to_registers() + # client.write_registers(address, registers, unit=1) + + # Or can write encoded binary string client.write_registers(address, payload, skip_encode=True, unit=1) # ----------------------------------------------------------------------- # # If you need to decode a collection of registers in a weird layout, the @@ -95,7 +116,7 @@ def run_binary_payload_ex(): # - an 8 bit int 0x12 # - an 8 bit bitstring [0,1,0,1,1,0,1,0] # ----------------------------------------------------------------------- # - address = 0x00 + address = 0x0 count = len(payload) result = client.read_holding_registers(address, count, unit=1) print("-" * 60) @@ -105,19 +126,26 @@ def run_binary_payload_ex(): print("\n") decoder = BinaryPayloadDecoder.fromRegisters(result.registers, byteorder=Endian.Little, - wordorder=Endian.Big) - decoded = { - 'string': decoder.decode_string(8), - 'float': decoder.decode_32bit_float(), - '16uint': decoder.decode_16bit_uint(), - 'ignored': decoder.skip_bytes(2), - '8int': decoder.decode_8bit_int(), - 'bits': decoder.decode_bits(), - "32uints": decoder.decode_32bit_uint(), - "32ints": decoder.decode_32bit_int(), - "64ints": decoder.decode_64bit_int(), - } - + wordorder=Endian.Little) + + decoded = OrderedDict([ + ('string', decoder.decode_string(8)), + ('bits', decoder.decode_bits()), + ('8int', decoder.decode_8bit_int()), + ('8uint', decoder.decode_8bit_uint()), + ('16int', decoder.decode_16bit_int()), + ('16uint', decoder.decode_16bit_uint()), + ('32int', decoder.decode_32bit_int()), + ('32uint', decoder.decode_32bit_uint()), + ('32float', decoder.decode_32bit_float()), + ('32float2', decoder.decode_32bit_float()), + ('64int', decoder.decode_64bit_int()), + ('64uint', decoder.decode_64bit_uint()), + ('ignore', decoder.skip_bytes(8)), + ('64float', decoder.decode_64bit_float()), + ('64float2', decoder.decode_64bit_float()), + ]) + print("-" * 60) print("Decoded Data") print("-" * 60) diff --git a/examples/common/modbus_payload_server.py b/examples/common/modbus_payload_server.py index 31c188e43..b2eb58e78 100755 --- a/examples/common/modbus_payload_server.py +++ b/examples/common/modbus_payload_server.py @@ -27,7 +27,9 @@ # configure the service logging # --------------------------------------------------------------------------- # import logging -logging.basicConfig() +FORMAT = ('%(asctime)-15s %(threadName)-15s' + ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') +logging.basicConfig(format=FORMAT) log = logging.getLogger() log.setLevel(logging.DEBUG) @@ -36,12 +38,24 @@ def run_payload_server(): # ----------------------------------------------------------------------- # # build your payload # ----------------------------------------------------------------------- # - builder = BinaryPayloadBuilder(byteorder=Endian.Little) - # builder.add_string('abcdefgh') - # builder.add_32bit_float(22.34) - # builder.add_16bit_uint(4660) - # builder.add_8bit_int(18) + builder = BinaryPayloadBuilder(byteorder=Endian.Little, + wordorder=Endian.Little) + builder.add_string('abcdefgh') builder.add_bits([0, 1, 0, 1, 1, 0, 1, 0]) + builder.add_8bit_int(-0x12) + builder.add_8bit_uint(0x12) + builder.add_16bit_int(-0x5678) + builder.add_16bit_uint(0x1234) + builder.add_32bit_int(-0x1234) + builder.add_32bit_uint(0x12345678) + builder.add_32bit_float(22.34) + builder.add_32bit_float(-22.34) + builder.add_64bit_int(-0xDEADBEEF) + builder.add_64bit_uint(0x12345678DEADBEEF) + builder.add_64bit_uint(0xDEADBEEFDEADBEED) + builder.add_64bit_float(123.45) + builder.add_64bit_float(-123.45) + # ----------------------------------------------------------------------- # # use that payload in the data store @@ -64,7 +78,7 @@ def run_payload_server(): identity.VendorUrl = 'http://github.com/bashwork/pymodbus/' identity.ProductName = 'Pymodbus Server' identity.ModelName = 'Pymodbus Server' - identity.MajorMinorRevision = '1.0' + identity.MajorMinorRevision = '1.5' # ----------------------------------------------------------------------- # # run the server you want # ----------------------------------------------------------------------- # diff --git a/examples/common/synchronous_client.py b/examples/common/synchronous_client.py index d583586b1..41578e1fc 100755 --- a/examples/common/synchronous_client.py +++ b/examples/common/synchronous_client.py @@ -24,7 +24,9 @@ # configure the client logging # --------------------------------------------------------------------------- # import logging -logging.basicConfig() +FORMAT = ('%(asctime)-15s %(threadName)-15s ' + '%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') +logging.basicConfig(format=FORMAT) log = logging.getLogger() log.setLevel(logging.DEBUG) @@ -60,8 +62,11 @@ def run_sync_client(): # client = ModbusClient('localhost', retries=3, retry_on_empty=True) # ------------------------------------------------------------------------# client = ModbusClient('localhost', port=5020) - # client = ModbusClient(method='ascii', port='/dev/pts/2', timeout=1) - # client = ModbusClient(method='rtu', port='/dev/ttyp0', timeout=1) + # client = ModbusClient('localhost', port=5020, framer=ModbusRtuFramer) + # client = ModbusClient(method='binary', port='/dev/ptyp0', timeout=1) + # client = ModbusClient(method='ascii', port='/dev/ptyp0', timeout=1) + # client = ModbusClient(method='rtu', port='/dev/ptyp0', timeout=1, + # baudrate=9600) client.connect() # ------------------------------------------------------------------------# @@ -72,9 +77,10 @@ def run_sync_client(): # which defaults to `0x00` # ----------------------------------------------------------------------- # log.debug("Reading Coils") - rr = client.read_coils(1, 1, unit=0x01) - print(rr) - + rr = client.read_coils(1, 1, unit=UNIT) + log.debug(rr) + + # ----------------------------------------------------------------------- # # example requests # ----------------------------------------------------------------------- # diff --git a/examples/common/synchronous_client_ext.py b/examples/common/synchronous_client_ext.py index 637873a6f..61d42ff0a 100755 --- a/examples/common/synchronous_client_ext.py +++ b/examples/common/synchronous_client_ext.py @@ -13,6 +13,7 @@ # from pymodbus.client.sync import ModbusTcpClient as ModbusClient # from pymodbus.client.sync import ModbusUdpClient as ModbusClient from pymodbus.client.sync import ModbusSerialClient as ModbusClient +from pymodbus.transaction import ModbusRtuFramer # --------------------------------------------------------------------------- # # import the extended messages to perform @@ -26,7 +27,9 @@ # configure the client logging # --------------------------------------------------------------------------- # import logging -logging.basicConfig() +FORMAT = ('%(asctime)-15s %(threadName)-15s ' + '%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') +logging.basicConfig(format=FORMAT) log = logging.getLogger() log.setLevel(logging.DEBUG) @@ -44,7 +47,9 @@ def execute_extended_requests(): # It should be noted that you can supply an ipv4 or an ipv6 host address # for both the UDP and TCP clients. # ------------------------------------------------------------------------# - client = ModbusClient(method='rtu', port="/dev/ttyp0") + client = ModbusClient(method='rtu', port="/dev/ptyp0") + # client = ModbusClient(method='ascii', port="/dev/ptyp0") + # client = ModbusClient(method='binary', port="/dev/ptyp0") # client = ModbusClient('127.0.0.1', port=5020) client.connect() @@ -73,7 +78,7 @@ def execute_extended_requests(): log.debug("Running ReadDeviceInformationRequest") rq = ReadDeviceInformationRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference # assert (rr.function_code < 0x80) # test that we are not an error # assert (rr.information[0] == b'Pymodbus') # test the vendor name @@ -83,7 +88,7 @@ def execute_extended_requests(): log.debug("Running ReportSlaveIdRequest") rq = ReportSlaveIdRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference # assert(rr.function_code < 0x80) # test that we are not an error # assert(rr.identifier == 0x00) # test the slave identifier @@ -92,7 +97,7 @@ def execute_extended_requests(): log.debug("Running ReadExceptionStatusRequest") rq = ReadExceptionStatusRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference # assert(rr.function_code < 0x80) # test that we are not an error # assert(rr.status == 0x55) # test the status code @@ -100,7 +105,7 @@ def execute_extended_requests(): log.debug("Running GetCommEventCounterRequest") rq = GetCommEventCounterRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference # assert(rr.function_code < 0x80) # test that we are not an error # assert(rr.status == True) # test the status code @@ -109,7 +114,7 @@ def execute_extended_requests(): log.debug("Running GetCommEventLogRequest") rq = GetCommEventLogRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference # assert(rr.function_code < 0x80) # test that we are not an error # assert(rr.status == True) # test the status code @@ -123,98 +128,98 @@ def execute_extended_requests(): log.debug("Running ReturnQueryDataRequest") rq = ReturnQueryDataRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference # assert(rr.message[0] == 0x0000) # test the resulting message log.debug("Running RestartCommunicationsOptionRequest") rq = RestartCommunicationsOptionRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference # assert(rr.message == 0x0000) # test the resulting message log.debug("Running ReturnDiagnosticRegisterRequest") rq = ReturnDiagnosticRegisterRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ChangeAsciiInputDelimiterRequest") rq = ChangeAsciiInputDelimiterRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ForceListenOnlyModeRequest") rq = ForceListenOnlyModeRequest(unit=UNIT) rr = client.execute(rq) # does not send a response - print(rr) + log.debug(rr) log.debug("Running ClearCountersRequest") rq = ClearCountersRequest() rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ReturnBusCommunicationErrorCountRequest") rq = ReturnBusCommunicationErrorCountRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ReturnBusExceptionErrorCountRequest") rq = ReturnBusExceptionErrorCountRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ReturnSlaveMessageCountRequest") rq = ReturnSlaveMessageCountRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ReturnSlaveNoResponseCountRequest") rq = ReturnSlaveNoResponseCountRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ReturnSlaveNAKCountRequest") rq = ReturnSlaveNAKCountRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ReturnSlaveBusyCountRequest") rq = ReturnSlaveBusyCountRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ReturnSlaveBusCharacterOverrunCountRequest") rq = ReturnSlaveBusCharacterOverrunCountRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ReturnIopOverrunCountRequest") rq = ReturnIopOverrunCountRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running ClearOverrunCountRequest") rq = ClearOverrunCountRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference log.debug("Running GetClearModbusPlusRequest") rq = GetClearModbusPlusRequest(unit=UNIT) rr = client.execute(rq) - print(rr) + log.debug(rr) # assert(rr == None) # not supported by reference # ------------------------------------------------------------------------# diff --git a/examples/common/synchronous_server.py b/examples/common/synchronous_server.py index ef08175e1..20fc13c4f 100755 --- a/examples/common/synchronous_server.py +++ b/examples/common/synchronous_server.py @@ -16,7 +16,7 @@ from pymodbus.server.sync import StartSerialServer from pymodbus.device import ModbusDeviceIdentification -from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSparseDataBlock from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext from pymodbus.transaction import ModbusRtuFramer, ModbusBinaryFramer @@ -24,14 +24,12 @@ # configure the service logging # --------------------------------------------------------------------------- # import logging -FORMAT = '%(asctime)-15s %(levelname)-8s %(module)-8s:%(lineno)-8s %(message)s' +FORMAT = ('%(asctime)-15s %(threadName)-15s' + ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') logging.basicConfig(format=FORMAT) log = logging.getLogger() log.setLevel(logging.DEBUG) -SERIAL_PORT = "/tmp/ptyp0" -TCP_PORT = 5020 - def run_server(): # ----------------------------------------------------------------------- # @@ -93,7 +91,8 @@ def run_server(): co=ModbusSequentialDataBlock(0, [17]*100), hr=ModbusSequentialDataBlock(0, [17]*100), ir=ModbusSequentialDataBlock(0, [17]*100)) - context = ModbusServerContext(slaves={1: store, 2: store}, single=False) + + context = ModbusServerContext(slaves=store, single=True) # ----------------------------------------------------------------------- # # initialize the server information @@ -106,29 +105,31 @@ def run_server(): identity.VendorUrl = 'http://github.com/riptideio/pymodbus/' identity.ProductName = 'Pymodbus Server' identity.ModelName = 'Pymodbus Server' - identity.MajorMinorRevision = '1.0' + identity.MajorMinorRevision = '1.5' # ----------------------------------------------------------------------- # # run the server you want # ----------------------------------------------------------------------- # # Tcp: - StartTcpServer(context, identity=identity, address=("localhost", TCP_PORT)) + StartTcpServer(context, identity=identity, address=("localhost", 5020)) # Udp: - # StartUdpServer(context, identity=identity, - # address=("localhost", TCP_PORT)) + # StartUdpServer(context, identity=identity, address=("localhost", 5020)) # Ascii: # StartSerialServer(context, identity=identity, - # port=SERIAL_PORT, timeout=1) - - # Binary: - # StartSerialServer(context, identity=identity, framer=ModbusBinaryFramer, - # port=SERIAL_PORT, timeout=1) - + # port='/dev/ttyp0', timeout=1) + # RTU: # StartSerialServer(context, framer=ModbusRtuFramer, identity=identity, - # port=SERIAL_PORT, timeout=.005, baudrate=9600) + # port='/dev/ttyp0', timeout=.005, baudrate=9600) + + # Binary + # StartSerialServer(context, + # identity=identity, + # framer=ModbusBinaryFramer, + # port='/dev/ttyp0', + # timeout=1) if __name__ == "__main__": diff --git a/examples/contrib/message_parser.py b/examples/contrib/message_parser.py index f44e36c46..73d109931 100755 --- a/examples/contrib/message_parser.py +++ b/examples/contrib/message_parser.py @@ -29,13 +29,16 @@ # -------------------------------------------------------------------------- # # -------------------------------------------------------------------------- # import logging -modbus_log = logging.getLogger("pymodbus") - +FORMAT = ('%(asctime)-15s %(threadName)-15s' + ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') +logging.basicConfig(format=FORMAT) +log = logging.getLogger() # -------------------------------------------------------------------------- # # build a quick wrapper around the framers # -------------------------------------------------------------------------- # + class Decoder(object): def __init__(self, framer, encode=False): @@ -60,8 +63,8 @@ def decode(self, message): print("Decoding Message %s" % value) print("="*80) decoders = [ - self.framer(ServerDecoder()), - self.framer(ClientDecoder()), + self.framer(ServerDecoder(), client=None), + self.framer(ClientDecoder(), client=None) ] for decoder in decoders: print("%s" % decoder.decoder.__class__.__name__) @@ -69,8 +72,9 @@ def decode(self, message): try: decoder.addToFrame(message) if decoder.checkFrame(): + unit = decoder._header.get("uid", 0x00) decoder.advanceFrame() - decoder.processIncomingPacket(message, self.report) + decoder.processIncomingPacket(message, self.report, unit) else: self.check_errors(decoder, message) except Exception as ex: @@ -81,7 +85,8 @@ def check_errors(self, decoder, message): :param message: The message to find errors in """ - pass + log.error("Unable to parse message - {} with {}".format(message, + decoder)) def report(self, message): """ The callback to print the message information diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index 6dc332f37..5e8541a7f 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -64,9 +64,13 @@ def __init__(self, framer=None, **kwargs): ''' self._connected = False self.framer = framer or ModbusSocketFramer(ClientDecoder()) + if isinstance(self.framer, type): + # Framer class not instance + self.framer = self.framer(ClientDecoder(), client=None) if isinstance(self.framer, ModbusSocketFramer): self.transaction = DictTransactionManager(self, **kwargs) - else: self.transaction = FifoTransactionManager(self, **kwargs) + else: + self.transaction = FifoTransactionManager(self, **kwargs) def connectionMade(self): ''' Called upon a successful client connection. @@ -90,7 +94,8 @@ def dataReceived(self, data): :param data: The data returned from the server ''' - self.framer.processIncomingPacket(data, self._handleResponse) + unit = self.framer.decode_data(data).get("uid", 0) + self.framer.processIncomingPacket(data, self._handleResponse, unit=unit) def execute(self, request): ''' Starts the producer to send the next request to @@ -111,7 +116,8 @@ def _handleResponse(self, reply): handler = self.transaction.getTransaction(tid) if handler: handler.callback(reply) - else: _logger.debug("Unrequested message: " + str(reply)) + else: + _logger.debug("Unrequested message: " + str(reply)) def _buildResponse(self, tid): ''' Helper method to return a deferred response @@ -163,7 +169,8 @@ def datagramReceived(self, data, params): :param params: The host parameters sending the datagram ''' _logger.debug("Datagram from: %s:%d" % params) - self.framer.processIncomingPacket(data, self._handleResponse) + unit = self.framer.decode_data(data).get("uid", 0) + self.framer.processIncomingPacket(data, self._handleResponse, unit=unit) def execute(self, request): ''' Starts the producer to send the next request to diff --git a/pymodbus/client/common.py b/pymodbus/client/common.py index b740da9c5..ea3c981c3 100644 --- a/pymodbus/client/common.py +++ b/pymodbus/client/common.py @@ -14,18 +14,7 @@ from pymodbus.file_message import * from pymodbus.other_message import * - -class ModbusTransactionState(object): - """ - Modbus Client States - """ - IDLE = 0 - SENDING = 1 - WAITING_FOR_REPLY = 2 - WAITING_TURNAROUND_DELAY = 3 - PROCESSING_REPLY = 4 - PROCESSING_ERROR = 5 - TRANSCATION_COMPLETE = 6 +from pymodbus.utilities import ModbusTransactionState class ModbusClientMixin(object): @@ -43,6 +32,9 @@ class ModbusClientMixin(object): client = ModbusClient(...) response = client.read_coils(1, 10) ''' + state = ModbusTransactionState.IDLE + last_frame_end = 0 + silent_interval = 0 def read_coils(self, address, count=1, **kwargs): ''' diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 554429187..213e6d22b 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -1,9 +1,10 @@ import socket import serial import time +import sys from pymodbus.constants import Defaults -from pymodbus.utilities import hexlify_packets +from pymodbus.utilities import hexlify_packets, ModbusTransactionState from pymodbus.factory import ClientDecoder from pymodbus.exceptions import NotImplementedException, ParameterException from pymodbus.exceptions import ConnectionException @@ -11,7 +12,7 @@ from pymodbus.transaction import DictTransactionManager from pymodbus.transaction import ModbusSocketFramer, ModbusBinaryFramer from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer -from pymodbus.client.common import ModbusClientMixin, ModbusTransactionState +from pymodbus.client.common import ModbusClientMixin # --------------------------------------------------------------------------- # # Logging @@ -19,21 +20,11 @@ import logging _logger = logging.getLogger(__name__) - -TransactionStateString = { - ModbusTransactionState.IDLE: "IDLE", - ModbusTransactionState.SENDING: "SENDING", - ModbusTransactionState.WAITING_FOR_REPLY: "WAITING_FOR_REPLY", - ModbusTransactionState.WAITING_TURNAROUND_DELAY: "WAITING_TURNAROUND_DELAY", - ModbusTransactionState.PROCESSING_REPLY: "PROCESSING_REPLY", - ModbusTransactionState.PROCESSING_ERROR: "PROCESSING_ERROR", - ModbusTransactionState.TRANSCATION_COMPLETE: "TRANSCATION_COMPLETE" -} - # --------------------------------------------------------------------------- # # The Synchronous Clients # --------------------------------------------------------------------------- # + class BaseModbusClient(ModbusClientMixin): """ Inteface for a modbus synchronous client. Defined here are all the @@ -41,7 +32,6 @@ class BaseModbusClient(ModbusClientMixin): simply need to implement the transport methods and set the correct framer. """ - def __init__(self, framer, **kwargs): """ Initialize a client instance @@ -52,6 +42,8 @@ def __init__(self, framer, **kwargs): self.transaction = DictTransactionManager(self, **kwargs) else: self.transaction = FifoTransactionManager(self, **kwargs) + self._debug = False + self._debugfd = None # ----------------------------------------------------------------------- # # Client interface @@ -68,6 +60,21 @@ def close(self): """ pass + def is_socket_open(self): + """ + Check whether the underlying socket/serial is open or not. + + :returns: True if socket/serial is open, False otherwise + """ + raise NotImplementedException( + "is_socket_open() not implemented by {}".format(self.__str__()) + ) + + def send(self, request): + _logger.debug("New Transaction state 'SENDING'") + self.state = ModbusTransactionState.SENDING + return self._send(request) + def _send(self, request): """ Sends data on the underlying socket @@ -76,6 +83,9 @@ def _send(self, request): """ raise NotImplementedException("Method not implemented by derived class") + def recv(self, size): + return self._recv(size) + def _recv(self, size): """ Reads data from the underlying descriptor @@ -112,6 +122,36 @@ def __exit__(self, klass, value, traceback): """ Implement the client with exit block """ self.close() + def idle_time(self): + if self.last_frame_end is None or self.silent_interval is None: + return 0 + return self.last_frame_end + self.silent_interval + + def debug_enabled(self): + """ + Returns a boolean indicating if debug is enabled. + """ + return self._debug + + def set_debug(self, debug): + """ + Sets the current debug flag. + """ + self._debug = debug + + def trace(self, writeable): + if writeable: + self.set_debug(True) + self._debugfd = writeable + + def _dump(self, data, direction): + fd = self._debugfd if self._debugfd else sys.stdout + try: + fd.write(hexlify_packets(data)) + except Exception as e: + self._logger.debug(hexlify_packets(data)) + self._logger.exception(e) + def __str__(self): """ Builds a string representation of the connection @@ -144,7 +184,7 @@ def __init__(self, host='127.0.0.1', port=Defaults.Port, self.source_address = kwargs.get('source_address', ('', 0)) self.socket = None self.timeout = kwargs.get('timeout', Defaults.Timeout) - BaseModbusClient.__init__(self, framer(ClientDecoder()), **kwargs) + BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **kwargs) def connect(self): """ Connect to the modbus tcp server @@ -192,6 +232,9 @@ def _recv(self, size): raise ConnectionException(self.__str__()) return self.socket.recv(size) + def is_socket_open(self): + return True if self.socket is not None else False + def __str__(self): """ Builds a string representation of the connection @@ -220,7 +263,7 @@ def __init__(self, host='127.0.0.1', port=Defaults.Port, self.port = port self.socket = None self.timeout = kwargs.get('timeout', None) - BaseModbusClient.__init__(self, framer(ClientDecoder()), **kwargs) + BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **kwargs) @classmethod def _get_address_family(cls, address): @@ -279,6 +322,9 @@ def _recv(self, size): raise ConnectionException(self.__str__()) return self.socket.recvfrom(size)[0] + def is_socket_open(self): + return True if self.socket is not None else False + def __str__(self): """ Builds a string representation of the connection @@ -314,7 +360,7 @@ def __init__(self, method='ascii', **kwargs): """ self.method = method self.socket = None - BaseModbusClient.__init__(self, self.__implementation(method), + BaseModbusClient.__init__(self, self.__implementation(method, self), **kwargs) self.port = kwargs.get('port', 0) @@ -323,16 +369,16 @@ def __init__(self, method='ascii', **kwargs): 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": - self._last_frame_end = None if self.baudrate > 19200: - self._silent_interval = 1.75/1000 # ms + self.silent_interval = 1.75 / 1000 # ms else: - self._silent_interval = 3.5 * (1 + 8 + 2) / self.baudrate - self._silent_interval = round(self._silent_interval, 6) + self.silent_interval = 3.5 * (1 + 8 + 2) / self.baudrate + self.silent_interval = round(self.silent_interval, 6) @staticmethod - def __implementation(method): + def __implementation(method, client): """ Returns the requested framer :method: The serial framer to instantiate @@ -340,13 +386,13 @@ def __implementation(method): """ method = method.lower() if method == 'ascii': - return ModbusAsciiFramer(ClientDecoder()) + return ModbusAsciiFramer(ClientDecoder(), client) elif method == 'rtu': - return ModbusRtuFramer(ClientDecoder()) + return ModbusRtuFramer(ClientDecoder(), client) elif method == 'binary': - return ModbusBinaryFramer(ClientDecoder()) + return ModbusBinaryFramer(ClientDecoder(), client) elif method == 'socket': - return ModbusSocketFramer(ClientDecoder()) + return ModbusSocketFramer(ClientDecoder(), client) raise ParameterException("Invalid framer method requested") def connect(self): @@ -367,7 +413,7 @@ def connect(self): _logger.error(msg) self.close() if self.method == "rtu": - self._last_frame_end = None + self.last_frame_end = None return self.socket is not None def close(self): @@ -388,31 +434,6 @@ def _send(self, request): :param request: The encoded request to send :return: The number of bytes written """ - _logger.debug("Current transaction " - "state - {}".format(TransactionStateString[self.state])) - while self.state != ModbusTransactionState.IDLE: - if self.state == ModbusTransactionState.TRANSCATION_COMPLETE: - ts = round(time.time(), 6) - _logger.debug("Changing state to IDLE - Last Frame End - {}, " - "Current Time stamp - {}".format( - self._last_frame_end, ts)) - if self.method == "rtu": - if self._last_frame_end: - idle_time = self._last_frame_end + self._silent_interval - if round(ts-idle_time, 6) <= self._silent_interval: - _logger.debug("Waiting for 3.5 char before next " - "send - {} ms".format( - self._silent_interval*1000)) - time.sleep(self._silent_interval) - else: - # Recovering from last error ?? - time.sleep(self._silent_interval) - self.state = ModbusTransactionState.IDLE - else: - _logger.debug("Sleeping") - time.sleep(self._silent_interval) - _logger.debug("Transaction state 'IDLE', intiating a new transaction") - self.state = ModbusTransactionState.SENDING if not self.socket: raise ConnectionException(self.__str__()) if request: @@ -433,11 +454,6 @@ def _send(self, request): pass size = self.socket.write(request) - _logger.debug("Changing transaction state from 'SENDING' " - "to 'WAITING FOR REPLY'") - self.state = ModbusTransactionState.WAITING_FOR_REPLY - if self.method == "rtu": - self._last_frame_end = round(time.time(), 6) return size return 0 @@ -450,14 +466,13 @@ def _recv(self, size): if not self.socket: raise ConnectionException(self.__str__()) result = self.socket.read(size) - if self.state != ModbusTransactionState.PROCESSING_REPLY: - _logger.debug("Changing transaction state from " - "'WAITING FOR REPLY' to 'PROCESSING REPLY'") - self.state = ModbusTransactionState.PROCESSING_REPLY - if self.method == "rtu": - self._last_frame_end = round(time.time(), 6) return result + def is_socket_open(self): + if self.socket: + return self.socket.is_open() + return False + def __str__(self): """ Builds a string representation of the connection diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index b80b3ceb4..b21d21d42 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -162,3 +162,7 @@ def __getitem__(self, slave): else: raise NoSuchSlaveException("slave - {} does not exist, " "or is out of range".format(slave)) + + def slaves(self): + # Python3 now returns keys() as iterable + return list(self._slaves.keys()) diff --git a/pymodbus/framer/__init__.py b/pymodbus/framer/__init__.py new file mode 100644 index 000000000..fb0530b8c --- /dev/null +++ b/pymodbus/framer/__init__.py @@ -0,0 +1,47 @@ +from pymodbus.interfaces import IModbusFramer +import struct + +# Unit ID, Function Code +BYTE_ORDER = '>' +FRAME_HEADER = 'BB' + +# Transaction Id, Protocol ID, Length, Unit ID, Function Code +SOCKET_FRAME_HEADER = BYTE_ORDER + 'HHH' + FRAME_HEADER + + +class ModbusFramer(IModbusFramer): + """ + Base Framer class + """ + + def _validate_unit_id(self, units, single): + """ + Validates if the received data is valid for the client + :param units: list of unit id for which the transaction is valid + :param single: Set to true to treat this as a single context + :return: """ + + if single: + return True + else: + if 0 in units or 0xFF in units: + # Handle Modbus TCP unit identifier (0x00 0r 0xFF) + # in async requests + return True + return self._header['uid'] in units + + def sendPacket(self, message): + """ + Sends packets on the bus with 3.5char delay between frames + :param message: Message to be sent over the bus + :return: + """ + return self.client.send(message) + + def recvPacket(self, size): + """ + Receives packet from the bus with specified len + :param size: Number of bytes to read + :return: + """ + return self.client.recv(size) diff --git a/pymodbus/framer/ascii_framer.py b/pymodbus/framer/ascii_framer.py new file mode 100644 index 000000000..9c2b1afda --- /dev/null +++ b/pymodbus/framer/ascii_framer.py @@ -0,0 +1,214 @@ +import struct +import socket +from binascii import b2a_hex, a2b_hex + +from pymodbus.exceptions import ModbusIOException +from pymodbus.utilities import checkLRC, computeLRC +from pymodbus.framer import ModbusFramer, FRAME_HEADER, BYTE_ORDER + +# Python 2 compatibility. +try: + TimeoutError +except NameError: + TimeoutError = socket.timeout + +ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER + +# --------------------------------------------------------------------------- # +# Logging +# --------------------------------------------------------------------------- # +import logging +_logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- # +# Modbus ASCII Message +# --------------------------------------------------------------------------- # +class ModbusAsciiFramer(ModbusFramer): + """ + Modbus ASCII Frame Controller:: + + [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] + 1c 2c 2c Nc 2c 2c + + * data can be 0 - 2x252 chars + * end is '\\r\\n' (Carriage return line feed), however the line feed + character can be changed via a special command + * start is ':' + + This framer is used for serial transmission. Unlike the RTU protocol, + the data in this framer is transferred in plain text ascii. + """ + + def __init__(self, decoder, client=None): + """ Initializes a new instance of the framer + + :param decoder: The decoder implementation to use + """ + self._buffer = b'' + self._header = {'lrc': '0000', 'len': 0, 'uid': 0x00} + self._hsize = 0x02 + self._start = b':' + self._end = b"\r\n" + self.decoder = decoder + self.client = client + + # ----------------------------------------------------------------------- # + # Private Helper Functions + # ----------------------------------------------------------------------- # + def decode_data(self, data): + if len(data) > 1: + uid = int(data[1:3], 16) + fcode = int(data[3:5], 16) + return dict(unit=uid, fcode=fcode) + return dict() + + def checkFrame(self): + """ Check and decode the next frame + + :returns: True if we successful, False otherwise + """ + start = self._buffer.find(self._start) + if start == -1: + return False + if start > 0: # go ahead and skip old bad data + self._buffer = self._buffer[start:] + start = 0 + + end = self._buffer.find(self._end) + if end != -1: + self._header['len'] = end + self._header['uid'] = int(self._buffer[1:3], 16) + self._header['lrc'] = int(self._buffer[end - 2:end], 16) + data = a2b_hex(self._buffer[start + 1:end - 2]) + return checkLRC(data, self._header['lrc']) + return False + + def advanceFrame(self): + """ Skip over the current framed message + This allows us to skip over the current message after we have processed + it or determined that it contains an error. It also has to reset the + current frame header handle + """ + self._buffer = self._buffer[self._header['len'] + 2:] + self._header = {'lrc': '0000', 'len': 0, 'uid': 0x00} + + def isFrameReady(self): + """ Check if we should continue decode logic + This is meant to be used in a while loop in the decoding phase to let + the decoder know that there is still data in the buffer. + + :returns: True if ready, False otherwise + """ + return len(self._buffer) > 1 + + def addToFrame(self, message): + """ Add the next message to the frame buffer + This should be used before the decoding while loop to add the received + data to the buffer handle. + + :param message: The most recent packet + """ + self._buffer += message + + def getFrame(self): + """ Get the next frame from the buffer + + :returns: The frame data or '' + """ + start = self._hsize + 1 + end = self._header['len'] - 2 + buffer = self._buffer[start:end] + if end > 0: + return a2b_hex(buffer) + return b'' + + def resetFrame(self): + """ Reset the entire message frame. + This allows us to skip ovver errors that may be in the stream. + It is hard to know if we are simply out of sync or if there is + an error in the stream as we have no way to check the start or + end of the message (python just doesn't have the resolution to + check for millisecond delays). + """ + self._buffer = b'' + self._header = {'lrc': '0000', 'len': 0, 'uid': 0x00} + + def populateResult(self, result): + """ Populates the modbus result header + + The serial packets do not have any header information + that is copied. + + :param result: The response packet + """ + result.unit_id = self._header['uid'] + + # ----------------------------------------------------------------------- # + # Public Member Functions + # ----------------------------------------------------------------------- # + def processIncomingPacket(self, data, callback, unit, **kwargs): + """ + The new packet processing pattern + + 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 + messages at a time instead of 1. + + The processed and decoded messages are pushed to the callback + function to process and send. + + :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 single: True or False (If True, ignore unit address validation) + + """ + if not isinstance(unit, (list, tuple)): + unit = [unit] + single = kwargs.get('single', False) + self.addToFrame(data) + while self.isFrameReady(): + if self.checkFrame(): + if self._validate_unit_id(unit, single): + frame = self.getFrame() + result = self.decoder.decode(frame) + if result is None: + raise ModbusIOException("Unable to decode response") + self.populateResult(result) + self.advanceFrame() + callback(result) # defer this + else: + _logger.error("Not a valid unit id - {}, " + "ignoring!!".format(self._header['uid'])) + self.resetFrame() + else: + break + + def buildPacket(self, message): + """ Creates a ready to send modbus packet + Built off of a modbus request/response + + :param message: The request/response to send + :return: The encoded packet + """ + encoded = message.encode() + buffer = struct.pack(ASCII_FRAME_HEADER, message.unit_id, + message.function_code) + checksum = computeLRC(encoded + buffer) + + packet = bytearray() + params = (message.unit_id, message.function_code) + packet.extend(self._start) + packet.extend(('%02x%02x' % params).encode()) + packet.extend(b2a_hex(encoded)) + packet.extend(('%02x' % checksum).encode()) + packet.extend(self._end) + return bytes(packet).upper() + + +# __END__ + diff --git a/pymodbus/framer/binary_framer.py b/pymodbus/framer/binary_framer.py new file mode 100644 index 000000000..b8602489f --- /dev/null +++ b/pymodbus/framer/binary_framer.py @@ -0,0 +1,227 @@ +import struct +from pymodbus.exceptions import ModbusIOException +from pymodbus.utilities import checkCRC, computeCRC +from pymodbus.framer import ModbusFramer, FRAME_HEADER, BYTE_ORDER + +# --------------------------------------------------------------------------- # +# Logging +# --------------------------------------------------------------------------- # +import logging +_logger = logging.getLogger(__name__) + +BINARY_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER + +# --------------------------------------------------------------------------- # +# Modbus Binary Message +# --------------------------------------------------------------------------- # + + +class ModbusBinaryFramer(ModbusFramer): + """ + Modbus Binary Frame Controller:: + + [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] + 1b 1b 1b Nb 2b 1b + + * data can be 0 - 2x252 chars + * end is '}' + * start is '{' + + The idea here is that we implement the RTU protocol, however, + instead of using timing for message delimiting, we use start + and end of message characters (in this case { and }). Basically, + this is a binary framer. + + The only case we have to watch out for is when a message contains + the { or } characters. If we encounter these characters, we + simply duplicate them. Hopefully we will not encounter those + characters that often and will save a little bit of bandwitch + without a real-time system. + + Protocol defined by jamod.sourceforge.net. + """ + + def __init__(self, decoder, client=None): + """ Initializes a new instance of the framer + + :param decoder: The decoder implementation to use + """ + self._buffer = b'' + self._header = {'crc': 0x0000, 'len': 0, 'uid': 0x00} + self._hsize = 0x01 + self._start = b'\x7b' # { + self._end = b'\x7d' # } + self._repeat = [b'}'[0], b'{'[0]] # python3 hack + self.decoder = decoder + self.client = client + + # ----------------------------------------------------------------------- # + # Private Helper Functions + # ----------------------------------------------------------------------- # + def decode_data(self, data): + if len(data) > self._hsize: + uid = struct.unpack('>B', data[1:2])[0] + fcode = struct.unpack('>B', data[2:3])[0] + return dict(unit=uid, fcode=fcode) + return dict() + + def checkFrame(self): + """ Check and decode the next frame + + :returns: True if we are successful, False otherwise + """ + start = self._buffer.find(self._start) + if start == -1: + return False + if start > 0: # go ahead and skip old bad data + self._buffer = self._buffer[start:] + + end = self._buffer.find(self._end) + if end != -1: + self._header['len'] = end + self._header['uid'] = struct.unpack('>B', self._buffer[1:2])[0] + self._header['crc'] = struct.unpack('>H', self._buffer[end - 2:end])[0] + data = self._buffer[start + 1:end - 2] + return checkCRC(data, self._header['crc']) + return False + + def advanceFrame(self): + """ Skip over the current framed message + This allows us to skip over the current message after we have processed + it or determined that it contains an error. It also has to reset the + current frame header handle + """ + self._buffer = self._buffer[self._header['len'] + 2:] + self._header = {'crc':0x0000, 'len':0, 'uid':0x00} + + def isFrameReady(self): + """ Check if we should continue decode logic + This is meant to be used in a while loop in the decoding phase to let + the decoder know that there is still data in the buffer. + + :returns: True if ready, False otherwise + """ + return len(self._buffer) > 1 + + def addToFrame(self, message): + """ Add the next message to the frame buffer + This should be used before the decoding while loop to add the received + data to the buffer handle. + + :param message: The most recent packet + """ + self._buffer += message + + def getFrame(self): + """ Get the next frame from the buffer + + :returns: The frame data or '' + """ + start = self._hsize + 1 + end = self._header['len'] - 2 + buffer = self._buffer[start:end] + if end > 0: + return buffer + return b'' + + def populateResult(self, result): + """ Populates the modbus result header + + The serial packets do not have any header information + that is copied. + + :param result: The response packet + """ + result.unit_id = self._header['uid'] + + # ----------------------------------------------------------------------- # + # Public Member Functions + # ----------------------------------------------------------------------- # + def processIncomingPacket(self, data, callback, unit, **kwargs): + """ + The new packet processing pattern + + 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 + messages at a time instead of 1. + + The processed and decoded messages are pushed to the callback + function to process and send. + + :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 single: True or False (If True, ignore unit address validation) + + """ + self.addToFrame(data) + if not isinstance(unit, (list, tuple)): + unit = [unit] + single = kwargs.get('single', False) + while self.isFrameReady(): + if self.checkFrame(): + if self._validate_unit_id(unit, single): + result = self.decoder.decode(self.getFrame()) + if result is None: + raise ModbusIOException("Unable to decode response") + self.populateResult(result) + self.advanceFrame() + callback(result) # defer or push to a thread? + else: + _logger.debug("Not a valid unit id - {}, " + "ignoring!!".format(self._header['uid'])) + self.resetFrame() + break + + else: + _logger.debug("Frame check failed, ignoring!!") + self.resetFrame() + break + + def buildPacket(self, message): + """ Creates a ready to send modbus packet + + :param message: The request/response to send + :returns: The encoded packet + """ + data = self._preflight(message.encode()) + packet = struct.pack(BINARY_FRAME_HEADER, + message.unit_id, + message.function_code) + data + packet += struct.pack(">H", computeCRC(packet)) + packet = self._start + packet + self._end + return packet + + def _preflight(self, data): + """ + Preflight buffer test + + This basically scans the buffer for start and end + tags and if found, escapes them. + + :param data: The message to escape + :returns: the escaped packet + """ + array = bytearray() + for d in data: + if d in self._repeat: + array.append(d) + array.append(d) + return bytes(array) + + def resetFrame(self): + """ Reset the entire message frame. + This allows us to skip ovver errors that may be in the stream. + It is hard to know if we are simply out of sync or if there is + an error in the stream as we have no way to check the start or + end of the message (python just doesn't have the resolution to + check for millisecond delays). + """ + self._buffer = b'' + self._header = {'crc': 0x0000, 'len': 0, 'uid': 0x00} + + +# __END__ diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py new file mode 100644 index 000000000..b39649ea4 --- /dev/null +++ b/pymodbus/framer/rtu_framer.py @@ -0,0 +1,330 @@ +import struct +import time + +from pymodbus.exceptions import ModbusIOException +from pymodbus.exceptions import InvalidMessageRecievedException +from pymodbus.utilities import checkCRC, computeCRC +from pymodbus.utilities import hexlify_packets, ModbusTransactionState +from pymodbus.compat import byte2int +from pymodbus.framer import ModbusFramer, FRAME_HEADER, BYTE_ORDER + +# --------------------------------------------------------------------------- # +# Logging +# --------------------------------------------------------------------------- # +import logging +_logger = logging.getLogger(__name__) + +RTU_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER + + +# --------------------------------------------------------------------------- # +# Modbus RTU Message +# --------------------------------------------------------------------------- # +class ModbusRtuFramer(ModbusFramer): + """ + Modbus RTU Frame controller:: + + [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ] + 3.5 chars 1b 1b Nb 2b 3.5 chars + + Wait refers to the amount of time required to transmit at least x many + characters. In this case it is 3.5 characters. Also, if we receive a + wait of 1.5 characters at any point, we must trigger an error message. + Also, it appears as though this message is little endian. The logic is + simplified as the following:: + + block-on-read: + read until 3.5 delay + check for errors + decode + + The following table is a listing of the baud wait times for the specified + baud rates:: + + ------------------------------------------------------------------ + Baud 1.5c (18 bits) 3.5c (38 bits) + ------------------------------------------------------------------ + 1200 13333.3 us 31666.7 us + 4800 3333.3 us 7916.7 us + 9600 1666.7 us 3958.3 us + 19200 833.3 us 1979.2 us + 38400 416.7 us 989.6 us + ------------------------------------------------------------------ + 1 Byte = start + 8 bits + parity + stop = 11 bits + (1/Baud)(bits) = delay seconds + """ + + def __init__(self, decoder, client): + """ Initializes a new instance of the framer + + :param decoder: The decoder factory implementation to use + """ + self._buffer = b'' + self._header = {'uid': 0x00, 'len': 0, 'crc': '0000'} + self._hsize = 0x01 + self._end = b'\x0d\x0a' + self._min_frame_size = 4 + self.decoder = decoder + self.client = client + + # ----------------------------------------------------------------------- # + # Private Helper Functions + # ----------------------------------------------------------------------- # + def decode_data(self, data): + if len(data) > self._hsize: + uid = byte2int(data[0]) + fcode = byte2int(data[1]) + return dict(unit=uid, fcode=fcode) + return dict() + + def checkFrame(self): + """ + Check if the next frame is available. + Return True if we were successful. + + 1. Populate header + 2. Discard frame if UID does not match + """ + try: + self.populateHeader() + frame_size = self._header['len'] + data = self._buffer[:frame_size - 2] + 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): + return False + + def advanceFrame(self): + """ + Skip over the current framed message + This allows us to skip over the current message after we have processed + it or determined that it contains an error. It also has to reset the + current frame header handle + """ + try: + self._buffer = self._buffer[self._header['len']:] + except KeyError: + # Error response, no header len found + self.resetFrame() + _logger.debug("Frame advanced, resetting header!!") + self._header = {} + + def resetFrame(self): + """ + Reset the entire message frame. + This allows us to skip over errors that may be in the stream. + It is hard to know if we are simply out of sync or if there is + an error in the stream as we have no way to check the start or + end of the message (python just doesn't have the resolution to + check for millisecond delays). + """ + _logger.debug("Resetting frame - Current Frame in " + "buffer - {}".format(hexlify_packets(self._buffer))) + self._buffer = b'' + self._header = {} + + def isFrameReady(self): + """ + Check if we should continue decode logic + This is meant to be used in a while loop in the decoding phase to let + the decoder know that there is still data in the buffer. + + :returns: True if ready, False otherwise + """ + return len(self._buffer) > self._hsize + + def populateHeader(self, data=None): + """ + Try to set the headers `uid`, `len` and `crc`. + + This method examines `self._buffer` and writes meta + information into `self._header`. It calculates only the + values for headers that are not already in the dictionary. + + Beware that this method will raise an IndexError if + `self._buffer` is not yet long enough. + """ + data = data if data else self._buffer + self._header['uid'] = byte2int(data[0]) + func_code = byte2int(data[1]) + pdu_class = self.decoder.lookupPduClass(func_code) + size = pdu_class.calculateRtuFrameSize(data) + self._header['len'] = size + self._header['crc'] = data[size - 2:size] + + def addToFrame(self, message): + """ + This should be used before the decoding while loop to add the received + data to the buffer handle. + + :param message: The most recent packet + """ + self._buffer += message + + def getFrame(self): + """ + Get the next frame from the buffer + + :returns: The frame data or '' + """ + start = self._hsize + end = self._header['len'] - 2 + buffer = self._buffer[start:end] + if end > 0: + _logger.debug("Getting Frame - {}".format(hexlify_packets(buffer))) + return buffer + return b'' + + def populateResult(self, result): + """ + Populates the modbus result header + + The serial packets do not have any header information + that is copied. + + :param result: The response packet + """ + result.unit_id = self._header['uid'] + + # ----------------------------------------------------------------------- # + # Public Member Functions + # ----------------------------------------------------------------------- # + def processIncomingPacket(self, data, callback, unit, **kwargs): + """ + The new packet processing pattern + + 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 + messages at a time instead of 1. + + The processed and decoded messages are pushed to the callback + function to process and send. + + :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 single: True or False (If True, ignore unit address validation) + """ + if not isinstance(unit, (list, tuple)): + unit = [unit] + self.addToFrame(data) + single = kwargs.get("single", False) + while True: + if self.isFrameReady(): + if self.checkFrame(): + if self._validate_unit_id(unit, single): + self._process(callback) + else: + _logger.debug("Not a valid unit id - {}, " + "ignoring!!".format(self._header['uid'])) + self.resetFrame() + + else: + # Could be an error response + if len(self._buffer): + # Possible error ??? + self._process(callback, error=True) + else: + if len(self._buffer): + # Possible error ??? + if self._header.get('len', 0) < 2: + self._process(callback, error=True) + break + + def buildPacket(self, message): + """ + Creates a ready to send modbus packet + + :param message: The populated request/response to send + """ + data = message.encode() + packet = struct.pack(RTU_FRAME_HEADER, + message.unit_id, + message.function_code) + data + packet += struct.pack(">H", computeCRC(packet)) + return packet + + def sendPacket(self, message): + """ + Sends packets on the bus with 3.5char delay between frames + :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.TRANSCATION_COMPLETE: + ts = round(time.time(), 6) + _logger.debug("Changing state to IDLE - Last Frame End - {}, " + "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: + _logger.debug("Waiting for 3.5 char before next " + "send - {} ms".format( + self.client.silent_interval * 1000) + ) + time.sleep(self.client.silent_interval) + else: + # Recovering from last error ?? + time.sleep(self.client.silent_interval) + 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 + + def recvPacket(self, size): + """ + Receives packet from the bus with specified len + :param size: Number of bytes to read + :return: + """ + result = self.client.recv(size) + # if self.client.state != ModbusTransactionState.PROCESSING_REPLY: + # _logger.debug("Changing transaction state from " + # "'WAITING FOR REPLY' to 'PROCESSING REPLY'") + # self.client.state = ModbusTransactionState.PROCESSING_REPLY + + self.client.last_frame_end = round(time.time(), 6) + return result + + def _process(self, callback, error=False): + """ + Process incoming packets irrespective error condition + """ + data = self.getRawFrame() if error else self.getFrame() + result = self.decoder.decode(data) + if result is None: + raise ModbusIOException("Unable to decode request") + elif error and result.function_code < 0x80: + raise InvalidMessageRecievedException(result) + else: + self.populateResult(result) + self.advanceFrame() + callback(result) # defer or push to a thread? + + def getRawFrame(self): + """ + Returns the complete buffer + """ + _logger.debug("Getting Raw Frame - " + "{}".format(hexlify_packets(self._buffer))) + return self._buffer + +# __END__ diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/socket_framer.py new file mode 100644 index 000000000..37e3bfe9d --- /dev/null +++ b/pymodbus/framer/socket_framer.py @@ -0,0 +1,217 @@ +import struct +from pymodbus.exceptions import ModbusIOException +from pymodbus.exceptions import InvalidMessageRecievedException +from pymodbus.utilities import hexlify_packets +from pymodbus.framer import ModbusFramer, SOCKET_FRAME_HEADER + +# --------------------------------------------------------------------------- # +# Logging +# --------------------------------------------------------------------------- # +import logging +_logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- # +# Modbus TCP Message +# --------------------------------------------------------------------------- # + + +class ModbusSocketFramer(ModbusFramer): + """ Modbus Socket Frame controller + + 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 ] + [ tid ][ pid ][ length ][ uid ] + 2b 2b 2b 1b 1b Nb + + while len(message) > 0: + tid, pid, length`, uid = struct.unpack(">HHHB", message) + request = message[0:7 + length - 1`] + message = [7 + length - 1:] + + * length = uid + function code + data + * The -1 is to account for the uid byte + """ + + def __init__(self, decoder, client=None): + """ Initializes a new instance of the framer + + :param decoder: The decoder factory implementation to use + """ + self._buffer = b'' + self._header = {'tid': 0, 'pid': 0, 'len': 0, 'uid': 0} + self._hsize = 0x07 + self.decoder = decoder + self.client = client + + # ----------------------------------------------------------------------- # + # Private Helper Functions + # ----------------------------------------------------------------------- # + def checkFrame(self): + """ + Check and decode the next frame Return true if we were successful + """ + if self.isFrameReady(): + (self._header['tid'], self._header['pid'], + self._header['len'], self._header['uid']) = struct.unpack( + '>HHHB', self._buffer[0:self._hsize]) + + # someone sent us an error? ignore it + if self._header['len'] < 2: + self.advanceFrame() + # we have at least a complete message, continue + elif len(self._buffer) - self._hsize + 1 >= self._header['len']: + return True + # we don't have enough of a message yet, wait + return False + + def advanceFrame(self): + """ Skip over the current framed message + This allows us to skip over the current message after we have processed + it or determined that it contains an error. It also has to reset the + current frame header handle + """ + length = self._hsize + self._header['len'] - 1 + self._buffer = self._buffer[length:] + self._header = {'tid': 0, 'pid': 0, 'len': 0, 'uid': 0} + + def isFrameReady(self): + """ Check if we should continue decode logic + This is meant to be used in a while loop in the decoding phase to let + the decoder factory know that there is still data in the buffer. + + :returns: True if ready, False otherwise + """ + return len(self._buffer) > self._hsize + + def addToFrame(self, message): + """ Adds new packet data to the current frame buffer + + :param message: The most recent packet + """ + self._buffer += message + + def getFrame(self): + """ Return the next frame from the buffered data + + :returns: The next full frame buffer + """ + length = self._hsize + self._header['len'] - 1 + return self._buffer[self._hsize:length] + + def populateResult(self, result): + """ + Populates the modbus result with the transport specific header + information (pid, tid, uid, checksum, etc) + + :param result: The response packet + """ + result.transaction_id = self._header['tid'] + result.protocol_id = self._header['pid'] + result.unit_id = self._header['uid'] + + # ----------------------------------------------------------------------- # + # Public Member Functions + # ----------------------------------------------------------------------- # + def decode_data(self, data): + if len(data) > self._hsize: + tid, pid, length, uid, fcode = struct.unpack(SOCKET_FRAME_HEADER, + data[0:self._hsize+1]) + return dict(tid=tid, pid=pid, lenght=length, unit=uid, fcode=fcode) + return dict() + + def processIncomingPacket(self, data, callback, unit, **kwargs): + """ + The new packet processing pattern + + 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 + messages at a time instead of 1. + + The processed and decoded messages are pushed to the callback + function to process and send. + + :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 single: True or False (If True, ignore unit address validation) + + """ + if not isinstance(unit, (list, tuple)): + unit = [unit] + single = kwargs.get("single", False) + _logger.debug("Processing: " + hexlify_packets(data)) + self.addToFrame(data) + while True: + if self.isFrameReady(): + if self.checkFrame(): + if self._validate_unit_id(unit, single): + self._process(callback) + else: + _logger.debug("Not a valid unit id - {}, " + "ignoring!!".format(self._header['uid'])) + self.resetFrame() + else: + _logger.debug("Frame check failed, ignoring!!") + self.resetFrame() + else: + if len(self._buffer): + # Possible error ??? + if self._header['len'] < 2: + self._process(callback, error=True) + break + + def _process(self, callback, error=False): + """ + Process incoming packets irrespective error condition + """ + data = self.getRawFrame() if error else self.getFrame() + result = self.decoder.decode(data) + if result is None: + raise ModbusIOException("Unable to decode request") + elif error and result.function_code < 0x80: + raise InvalidMessageRecievedException(result) + else: + self.populateResult(result) + self.advanceFrame() + callback(result) # defer or push to a thread? + + def resetFrame(self): + """ + Reset the entire message frame. + This allows us to skip ovver errors that may be in the stream. + It is hard to know if we are simply out of sync or if there is + an error in the stream as we have no way to check the start or + end of the message (python just doesn't have the resolution to + check for millisecond delays). + """ + self._buffer = b'' + self._header = {'tid': 0, 'pid': 0, 'len': 0, 'uid': 0} + + def getRawFrame(self): + """ + Returns the complete buffer + """ + return self._buffer + + def buildPacket(self, message): + """ Creates a ready to send modbus packet + + :param message: The populated request/response to send + """ + data = message.encode() + packet = struct.pack(SOCKET_FRAME_HEADER, + message.transaction_id, + message.protocol_id, + len(data) + 2, + message.unit_id, + message.function_code) + packet += data + return packet + + +# __END__ diff --git a/pymodbus/payload.py b/pymodbus/payload.py index 354b793d1..d237d3471 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -4,6 +4,8 @@ A collection of utilities for building and decoding modbus messages payloads. + + """ from struct import pack, unpack from pymodbus.interfaces import IPayloadBuilder @@ -13,6 +15,23 @@ from pymodbus.utilities import make_byte_string from pymodbus.exceptions import ParameterException +# --------------------------------------------------------------------------- # +# Logging +# --------------------------------------------------------------------------- # +import logging +_logger = logging.getLogger(__name__) + + +WC = { + "b": 1, + "h": 2, + "i": 4, + "l": 4, + "q": 8, + "f": 4, + "d": 8 +} + class BinaryPayloadBuilder(IPayloadBuilder): """ @@ -22,41 +41,52 @@ class BinaryPayloadBuilder(IPayloadBuilder): time looking up the format strings. What follows is a simple example:: - builder = BinaryPayloadBuilder(endian=Endian.Little) + builder = BinaryPayloadBuilder(byteorder=Endian.Little) builder.add_8bit_uint(1) builder.add_16bit_uint(2) payload = builder.build() """ def __init__(self, payload=None, byteorder=Endian.Little, - wordorder=Endian.Big): + wordorder=Endian.Big, repack=False): """ Initialize a new instance of the payload builder :param payload: Raw binary payload data to initialize with :param byteorder: The endianess of the bytes in the words :param wordorder: The endianess of the word (when wordcount is >= 2) + :param repack: Repack the provided payload based on BO """ self._payload = payload or [] self._byteorder = byteorder self._wordorder = wordorder + self._repack = repack def _pack_words(self, fstring, value): """ - Packs Words based on the word order + Packs Words based on the word order and byte order + + # ---------------------------------------------- # + # pack in to network ordered value # + # unpack in to network ordered unsigned integer # + # Change Word order if little endian word order # + # Pack values back based on correct byte order # + # ---------------------------------------------- # + :param value: Value to be packed :return: """ - payload = pack(fstring, value) + value = pack("!{}".format(fstring), value) + wc = WC.get(fstring.lower())//2 + up = "!{}H".format(wc) + payload = unpack(up, value) + if self._wordorder == Endian.Little: - payload = [payload[i:i + 2] for i in range(0, len(payload), 2)] - if self._byteorder == Endian.Big: - payload = b''.join(list(reversed(payload))) - else: - payload = b''.join(payload) - - elif self._wordorder == Endian.Big and self._byteorder == Endian.Little: - payload = [payload[i:i + 2] for i in range(0, len(payload), 2)] - payload = b''.join(list(reversed(payload))) + payload = list(reversed(payload)) + + fstring = self._byteorder + "H" + payload = [pack(fstring, word) for word in payload] + payload = b''.join(payload) + return payload def to_string(self): @@ -84,9 +114,15 @@ def to_registers(self): :returns: The register layout to use as a block """ - fstring = self._byteorder + 'H' + # fstring = self._byteorder+'H' + fstring = '!H' payload = self.build() - return [unpack(fstring, value)[0] for value in payload] + if self._repack: + payload = [unpack(self._byteorder+"H", value)[0] for value in payload] + else: + payload = [unpack(fstring, value)[0] for value in payload] + _logger.debug(payload) + return payload def build(self): """ Return the payload buffer as a list @@ -134,7 +170,8 @@ def add_32bit_uint(self, value): :param value: The value to add to the buffer """ - fstring = self._byteorder + 'I' + fstring = 'I' + # fstring = self._byteorder + 'I' p_string = self._pack_words(fstring, value) self._payload.append(p_string) @@ -143,7 +180,7 @@ def add_64bit_uint(self, value): :param value: The value to add to the buffer """ - fstring = self._byteorder + 'Q' + fstring = 'Q' p_string = self._pack_words(fstring, value) self._payload.append(p_string) @@ -168,7 +205,7 @@ def add_32bit_int(self, value): :param value: The value to add to the buffer """ - fstring = self._byteorder + 'i' + fstring = 'i' p_string = self._pack_words(fstring, value) self._payload.append(p_string) @@ -177,7 +214,7 @@ def add_64bit_int(self, value): :param value: The value to add to the buffer """ - fstring = self._byteorder + 'q' + fstring = 'q' p_string = self._pack_words(fstring, value) self._payload.append(p_string) @@ -186,7 +223,7 @@ def add_32bit_float(self, value): :param value: The value to add to the buffer """ - fstring = self._byteorder + 'f' + fstring = 'f' p_string = self._pack_words(fstring, value) self._payload.append(p_string) @@ -195,7 +232,7 @@ def add_64bit_float(self, value): :param value: The value to add to the buffer """ - fstring = self._byteorder + 'd' + fstring = 'd' p_string = self._pack_words(fstring, value) self._payload.append(p_string) @@ -248,6 +285,7 @@ def fromRegisters(klass, registers, byteorder=Endian.Little, :param wordorder: The endianess of the word (when wordcount is >= 2) :returns: An initialized PayloadDecoder """ + _logger.debug(registers) if isinstance(registers, list): # repack into flat binary payload = b''.join(pack('!H', x) for x in registers) return klass(payload, byteorder, wordorder) @@ -271,21 +309,28 @@ def fromCoils(klass, coils, byteorder=Endian.Little): def _unpack_words(self, fstring, handle): """ - Packs Words based on the word order + Un Packs Words based on the word order and byte order + + # ---------------------------------------------- # + # Unpack in to network ordered unsigned integer # + # Change Word order if little endian word order # + # Pack values back based on correct byte order # + # ---------------------------------------------- # :param handle: Value to be unpacked :return: """ handle = make_byte_string(handle) + wc = WC.get(fstring.lower())//2 + up = "!{}H".format(wc) + handle = unpack(up, handle) if self._wordorder == Endian.Little: - handle = [handle[i:i + 2] for i in range(0, len(handle), 2)] - if self._byteorder == Endian.Big: - handle = b''.join(list(reversed(handle))) - else: - handle = b''.join(handle) - elif self._wordorder == Endian.Big and self._byteorder == Endian.Little: - handle = [handle[i:i + 2] for i in range(0, len(handle), 2)] - handle = b''.join(list(reversed(handle))) + handle = list(reversed(handle)) + # Repack as unsigned Integer + pk = self._byteorder + 'H' + handle = [pack(pk, p) for p in handle] + handle = b''.join(handle) + _logger.debug(handle) return handle def reset(self): @@ -324,19 +369,20 @@ def decode_32bit_uint(self): """ Decodes a 32 bit unsigned int from the buffer """ self._pointer += 4 - fstring = self._byteorder + 'I' + fstring = 'I' + # fstring = 'I' handle = self._payload[self._pointer - 4:self._pointer] handle = self._unpack_words(fstring, handle) - return unpack(fstring, handle)[0] + return unpack("!"+fstring, handle)[0] def decode_64bit_uint(self): """ Decodes a 64 bit unsigned int from the buffer """ self._pointer += 8 - fstring = self._byteorder + 'Q' + fstring = 'Q' handle = self._payload[self._pointer - 8:self._pointer] handle = self._unpack_words(fstring, handle) - return unpack(fstring, handle)[0] + return unpack("!"+fstring, handle)[0] def decode_8bit_int(self): """ Decodes a 8 bit signed int from the buffer @@ -360,37 +406,37 @@ def decode_32bit_int(self): """ Decodes a 32 bit signed int from the buffer """ self._pointer += 4 - fstring = self._byteorder + 'i' + fstring = 'i' handle = self._payload[self._pointer - 4:self._pointer] handle = self._unpack_words(fstring, handle) - return unpack(fstring, handle)[0] + return unpack("!"+fstring, handle)[0] def decode_64bit_int(self): """ Decodes a 64 bit signed int from the buffer """ self._pointer += 8 - fstring = self._byteorder + 'q' + fstring = 'q' handle = self._payload[self._pointer - 8:self._pointer] handle = self._unpack_words(fstring, handle) - return unpack(fstring, handle)[0] + return unpack("!"+fstring, handle)[0] def decode_32bit_float(self): """ Decodes a 32 bit float from the buffer """ self._pointer += 4 - fstring = self._byteorder + 'f' + fstring = 'f' handle = self._payload[self._pointer - 4:self._pointer] handle = self._unpack_words(fstring, handle) - return unpack(fstring, handle)[0] + return unpack("!"+fstring, handle)[0] def decode_64bit_float(self): """ Decodes a 64 bit float(double) from the buffer """ self._pointer += 8 - fstring = self._byteorder + 'd' + fstring = 'd' handle = self._payload[self._pointer - 8:self._pointer] handle = self._unpack_words(fstring, handle) - return unpack(fstring, handle)[0] + return unpack("!"+fstring, handle)[0] def decode_string(self, size=1): """ Decodes a string from the buffer @@ -398,7 +444,8 @@ def decode_string(self, size=1): :param size: The size of the string to decode """ self._pointer += size - return self._payload[self._pointer - size:self._pointer] + s = self._payload[self._pointer - size:self._pointer] + return s def skip_bytes(self, nbytes): """ Skip n bytes in the buffer diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 94921d5c3..3e52586db 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -1,8 +1,8 @@ -''' +""" Implementation of a Twisted Modbus Server ------------------------------------------ -''' +""" from binascii import b2a_hex from twisted.internet import protocol from twisted.internet.protocol import ServerFactory @@ -16,59 +16,64 @@ from pymodbus.device import ModbusAccessControl from pymodbus.device import ModbusDeviceIdentification from pymodbus.exceptions import NoSuchSlaveException -from pymodbus.transaction import ModbusSocketFramer, ModbusAsciiFramer +from pymodbus.transaction import (ModbusSocketFramer, + ModbusRtuFramer, + ModbusAsciiFramer, + ModbusBinaryFramer) from pymodbus.pdu import ModbusExceptions as merror from pymodbus.internal.ptwisted import InstallManagementConsole -from pymodbus.compat import byte2int from pymodbus.compat import IS_PYTHON3 -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Logging -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # import logging _logger = logging.getLogger(__name__) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Modbus TCP Server -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusTcpProtocol(protocol.Protocol): - ''' Implements a modbus server in twisted ''' + """ Implements a modbus server in twisted """ def connectionMade(self): - ''' Callback for when a client connects + """ Callback for when a client connects ..note:: since the protocol factory cannot be accessed from the protocol __init__, the client connection made is essentially our __init__ method. - ''' + """ _logger.debug("Client Connected [%s]" % self.transport.getHost()) - self.framer = self.factory.framer(decoder=self.factory.decoder) + self.framer = self.factory.framer(decoder=self.factory.decoder, + client=None) def connectionLost(self, reason): - ''' Callback for when a client disconnects + """ Callback for when a client disconnects :param reason: The client's reason for disconnecting - ''' + """ _logger.debug("Client Disconnected: %s" % reason) def dataReceived(self, data): - ''' Callback when we receive any data + """ Callback when we receive any data :param data: The data sent by the client - ''' + """ if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Data Received: ' + hexlify_packets(data)) if not self.factory.control.ListenOnly: - unit_address = byte2int(data[6]) - if unit_address in self.factory.store: - self.framer.processIncomingPacket(data, self._execute) + units = self.factory.store.slaves() + single = self.factory.store.single + self.framer.processIncomingPacket(data, self._execute, + single=single, + unit=units) def _execute(self, request): - ''' Executes the request and returns the result + """ Executes the request and returns the result :param request: The decoded request message - ''' + """ try: context = self.factory.store[request.unit_id] response = request.execute(context) @@ -80,16 +85,16 @@ def _execute(self, request): except Exception as ex: _logger.debug("Datastore unable to fulfill request: %s" % ex) response = request.doException(merror.SlaveFailure) - #self.framer.populateResult(response) + response.transaction_id = request.transaction_id response.unit_id = request.unit_id self._send(response) def _send(self, message): - ''' Send a request (string) to the network + """ Send a request (string) to the network :param message: The unencoded modbus response - ''' + """ if message.should_respond: self.factory.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) @@ -99,17 +104,17 @@ def _send(self, message): class ModbusServerFactory(ServerFactory): - ''' + """ Builder class for a modbus server This also holds the server datastore so that it is persisted between connections - ''' + """ protocol = ModbusTcpProtocol def __init__(self, store, framer=None, identity=None, **kwargs): - ''' Overloaded initializer for the modbus factory + """ Overloaded initializer for the modbus factory If the identify structure is not passed in, the ModbusControlBlock uses its own empty structure. @@ -118,7 +123,7 @@ def __init__(self, store, framer=None, identity=None, **kwargs): :param framer: The framer strategy to use :param identity: An optional identify structure :param ignore_missing_slaves: True to not send errors on a request to a missing slave - ''' + """ self.decoder = ServerDecoder() self.framer = framer or ModbusSocketFramer self.store = store or ModbusServerContext() @@ -130,14 +135,14 @@ def __init__(self, store, framer=None, identity=None, **kwargs): self.control.Identity.update(identity) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Modbus UDP Server -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusUdpProtocol(protocol.DatagramProtocol): - ''' Implements a modbus udp server in twisted ''' + """ Implements a modbus udp server in twisted """ def __init__(self, store, framer=None, identity=None, **kwargs): - ''' Overloaded initializer for the modbus factory + """ Overloaded initializer for the modbus factory If the identify structure is not passed in, the ModbusControlBlock uses its own empty structure. @@ -145,23 +150,25 @@ def __init__(self, store, framer=None, identity=None, **kwargs): :param store: The ModbusServerContext datastore :param framer: The framer strategy to use :param identity: An optional identify structure - :param ignore_missing_slaves: True to not send errors on a request to a missing slave - ''' + :param ignore_missing_slaves: True to not send errors on a request to + a missing slave + """ framer = framer or ModbusSocketFramer self.framer = framer(decoder=ServerDecoder()) self.store = store or ModbusServerContext() self.control = ModbusControlBlock() self.access = ModbusAccessControl() - self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) + self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', + Defaults.IgnoreMissingSlaves) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) def datagramReceived(self, data, addr): - ''' Callback when we receive any data + """ Callback when we receive any data :param data: The data sent by the client - ''' + """ _logger.debug("Client Connected [%s]" % addr) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("Datagram Received: "+ hexlify_packets(data)) @@ -170,15 +177,16 @@ def datagramReceived(self, data, addr): self.framer.processIncomingPacket(data, continuation) def _execute(self, request, addr): - ''' Executes the request and returns the result + """ Executes the request and returns the result :param request: The decoded request message - ''' + """ try: context = self.store[request.unit_id] response = request.execute(context) except NoSuchSlaveException as ex: - _logger.debug("requested slave does not exist: %s" % request.unit_id ) + _logger.debug("requested slave does not exist: " + "%s" % request.unit_id ) if self.ignore_missing_slaves: return # the client will simply timeout waiting for a response response = request.doException(merror.GatewayNoResponse) @@ -191,11 +199,11 @@ def _execute(self, request, addr): self._send(response, addr) def _send(self, message, addr): - ''' Send a request (string) to the network + """ Send a request (string) to the network :param message: The unencoded modbus response :param addr: The (host, port) to send the message to - ''' + """ self.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) if _logger.isEnabledFor(logging.DEBUG): @@ -203,9 +211,9 @@ def _send(self, message, addr): return self.transport.write(pdu, addr) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Starting Factories -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # def _is_main_thread(): import threading @@ -221,50 +229,62 @@ def _is_main_thread(): return True -def StartTcpServer(context, identity=None, address=None, console=False, **kwargs): - ''' Helper method to start the Modbus Async TCP server +def StartTcpServer(context, identity=None, address=None, + console=False, defer_reactor_run=False, **kwargs): + """ 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 to a missing slave - ''' + :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 + of starting server, to be explictly started by the user + """ from twisted.internet import reactor address = address or ("", Defaults.Port) - framer = ModbusSocketFramer + framer = kwargs.pop("framer", ModbusSocketFramer) factory = ModbusServerFactory(context, framer, identity, **kwargs) if console: InstallManagementConsole({'factory': factory}) _logger.info("Starting Modbus TCP Server on %s:%s" % address) reactor.listenTCP(address[1], factory, interface=address[0]) - reactor.run(installSignalHandlers=_is_main_thread()) + if not defer_reactor_run: + reactor.run(installSignalHandlers=_is_main_thread()) -def StartUdpServer(context, identity=None, address=None, **kwargs): - ''' Helper method to start the Modbus Async Udp server +def StartUdpServer(context, identity=None, address=None, + defer_reactor_run=False, **kwargs): + """ 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 to a missing slave - ''' + :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 + of starting server, to be explictly started by the user + """ from twisted.internet import reactor address = address or ("", Defaults.Port) - framer = ModbusSocketFramer + framer = kwargs.pop("framer", ModbusSocketFramer) server = ModbusUdpProtocol(context, framer, identity, **kwargs) _logger.info("Starting Modbus UDP Server on %s:%s" % address) reactor.listenUDP(address[1], server, interface=address[0]) - reactor.run(installSignalHandlers=_is_main_thread()) + if not defer_reactor_run: + reactor.run(installSignalHandlers=_is_main_thread()) def StartSerialServer(context, identity=None, - framer=ModbusAsciiFramer, **kwargs): - ''' Helper method to start the Modbus Async Serial server + framer=ModbusAsciiFramer, + defer_reactor_run=False, + **kwargs): + """ Helper method to start the Modbus Async Serial server :param context: The server data context :param identify: The server identity to use (default empty) @@ -272,8 +292,11 @@ 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 missing slave - ''' + :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 + of starting server, to be explictly started by the user + """ from twisted.internet import reactor from twisted.internet.serialport import SerialPort @@ -287,9 +310,10 @@ def StartSerialServer(context, identity=None, InstallManagementConsole({'factory': factory}) protocol = factory.buildProtocol(None) - SerialPort.getHost = lambda self: port # hack for logging + SerialPort.getHost = lambda self: port # hack for logging SerialPort(protocol, port, reactor, baudrate) - reactor.run(installSignalHandlers=_is_main_thread()) + if not defer_reactor_run: + reactor.run(installSignalHandlers=_is_main_thread()) def StopServer(): @@ -305,10 +329,9 @@ def StopServer(): _logger.debug("Stopping current thread") - -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Exported symbols -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # __all__ = [ "StartTcpServer", "StartUdpServer", "StartSerialServer", "StopServer" ] diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 2a9c6c3c9..d22f76cdf 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -44,7 +44,7 @@ def setup(self): """ _logger.debug("Client Connected [%s:%s]" % self.client_address) self.running = True - self.framer = self.server.framer(self.server.decoder) + self.framer = self.server.framer(self.server.decoder, client=None) self.server.threads.append(self) def finish(self): @@ -64,7 +64,7 @@ def execute(self, request): except NoSuchSlaveException as ex: _logger.debug("requested slave does not exist: %s" % request.unit_id ) if self.server.ignore_missing_slaves: - return # the client will simply timeout waiting for a response + return # the client will simply timeout waiting for a response response = request.doException(merror.GatewayNoResponse) except Exception as ex: _logger.debug("Datastore unable to fulfill request: %s; %s", ex, traceback.format_exc() ) @@ -102,28 +102,15 @@ def handle(self): try: data = self.request.recv(1024) if data: - if _logger.isEnabledFor(logging.DEBUG): - _logger.debug("recv: " + hexlify_packets(data)) - - if isinstance(self.framer, ModbusAsciiFramer): - unit_address = int(data[1:3], 16) - elif isinstance(self.framer, ModbusBinaryFramer): - unit_address = byte2int(data[1]) - elif isinstance(self.framer, ModbusRtuFramer): - unit_address = byte2int(data[0]) - elif isinstance(self.framer, ModbusSocketFramer): - unit_address = byte2int(data[6]) - else: - _logger.error("Unknown" - " framer - {}".format(type(self.framer))) - unit_address = -1 - if unit_address in self.server.context: - self.framer.processIncomingPacket(data, self.execute) + units = self.server.context.slaves() + single = self.server.context.single + self.framer.processIncomingPacket(data, self.execute, + units, single=single) except Exception as msg: - # since we only have a single socket, we cannot exit + # Since we only have a single socket, we cannot exit # Clear frame buffer self.framer.resetFrame() - _logger.error("Socket error occurred %s" % msg) + _logger.debug("Error: Socket error occurred %s" % msg) def send(self, message): """ Send a request (string) to the network @@ -176,11 +163,16 @@ def handle(self): while self.running: try: data = self.request.recv(1024) - if not data: self.running = False + if not data: + self.running = False if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Handling data: ' + hexlify_packets(data)) # if not self.server.control.ListenOnly: - self.framer.processIncomingPacket(data, self.execute) + + units = self.server.context.slaves() + single = self.server.context.single + self.framer.processIncomingPacket(data, self.execute, units, + single=single) except socket.timeout as msg: if _logger.isEnabledFor(logging.DEBUG): _logger.debug("Socket timeout occurred %s", msg) @@ -232,7 +224,10 @@ def handle(self): if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Handling data: ' + hexlify_packets(data)) # if not self.server.control.ListenOnly: - self.framer.processIncomingPacket(data, self.execute) + units = self.server.context.slaves() + single = self.server.context.single + self.framer.processIncomingPacket(data, self.execute, + units, single=single) except socket.timeout: pass except socket.error as msg: _logger.error("Socket error occurred %s" % msg) @@ -243,6 +238,8 @@ def handle(self): self.running = False reset_frame = True finally: + # Reset data after processing + self.request = (None, self.socket) if reset_frame: self.framer.resetFrame() reset_frame = False @@ -303,6 +300,7 @@ def __init__(self, context, framer=None, identity=None, socketserver.ThreadingTCPServer.__init__(self, self.address, self.handler) + # self._BaseServer__shutdown_request = True def process_request(self, request, client): """ Callback for connecting a new client thread @@ -371,6 +369,7 @@ def __init__(self, context, framer=None, identity=None, address=None, socketserver.ThreadingUDPServer.__init__(self, self.address, self.handler) + self._BaseServer__shutdown_request = True def process_request(self, request, client): """ Callback for connecting a new client thread @@ -511,7 +510,7 @@ def StartTcpServer(context=None, identity=None, address=None, **kwargs): :param address: An optional (interface, port) to bind to. :param ignore_missing_slaves: True to not send errors on a request to a missing slave """ - framer = ModbusSocketFramer + framer = kwargs.pop("framer", ModbusSocketFramer) server = ModbusTcpServer(context, framer, identity, address, **kwargs) server.serve_forever() @@ -554,6 +553,7 @@ def StartSerialServer(context=None, identity=None, **kwargs): # Exported symbols # --------------------------------------------------------------------------- # + __all__ = [ "StartTcpServer", "StartUdpServer", "StartSerialServer" ] diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index b7eb6b350..3cfb4b91a 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -3,18 +3,18 @@ ''' import struct import socket -from binascii import b2a_hex, a2b_hex -from serial import SerialException from threading import RLock + from pymodbus.exceptions import ModbusIOException, NotImplementedException from pymodbus.exceptions import InvalidMessageRecievedException from pymodbus.constants import Defaults -from pymodbus.interfaces import IModbusFramer -from pymodbus.utilities import checkCRC, computeCRC -from pymodbus.utilities import checkLRC, computeLRC -from pymodbus.utilities import hexlify_packets -from pymodbus.compat import iterkeys, imap, byte2int -from pymodbus.client.common import ModbusTransactionState +from pymodbus.framer.ascii_framer import ModbusAsciiFramer +from pymodbus.framer.rtu_framer import ModbusRtuFramer +from pymodbus.framer.socket_framer import ModbusSocketFramer +from pymodbus.framer.binary_framer import ModbusBinaryFramer +from pymodbus.utilities import hexlify_packets, ModbusTransactionState +from pymodbus.compat import iterkeys, byte2int + # Python 2 compatibility. try: @@ -22,16 +22,16 @@ except NameError: TimeoutError = socket.timeout -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Logging -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # import logging _logger = logging.getLogger(__name__) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # The Global Transaction Manager -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusTransactionManager(object): ''' Impelements a transaction for a manager @@ -117,6 +117,9 @@ def execute(self, request): ''' with self._transaction_lock: try: + _logger.debug("Current transaction state - {}".format( + ModbusTransactionState.to_string(self.client.state)) + ) retries = self.retries request.transaction_id = self.getNextTID() _logger.debug("Running transaction %d" % request.transaction_id) @@ -162,7 +165,8 @@ def execute(self, request): continue break self.client.framer.processIncomingPacket(response, - self.addTransaction) + self.addTransaction, + request.unit_id) response = self.getTransaction(request.transaction_id) if not response: if len(self.transactions): @@ -196,7 +200,11 @@ def _transact(self, packet, response_length, full=False): packet = self.client.framer.buildPacket(packet) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("SEND: " + hexlify_packets(packet)) - self._send(packet) + size = self._send(packet) + if size: + _logger.debug("Changing transaction state from 'SENDING' " + "to 'WAITING FOR REPLY'") + self.client.state = ModbusTransactionState.WAITING_FOR_REPLY result = self._recv(response_length or 1024, full) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("RECV: " + hexlify_packets(result)) @@ -209,11 +217,9 @@ def _transact(self, packet, response_length, full=False): return result, last_exception def _send(self, packet): - return self.client._send(packet) + return self.client.framer.sendPacket(packet) def _recv(self, expected_response_length, full): - # retries = self.retries - # exception = False expected_response_length = expected_response_length or 1024 if not full: exception_length = self._calculate_exception_length() @@ -228,7 +234,7 @@ def _recv(self, expected_response_length, full): else: min_size = expected_response_length - read_min = self.client._recv(min_size) + read_min = self.client.framer.recvPacket(min_size) if read_min: if isinstance(self.client.framer, ModbusSocketFramer): func_code = byte2int(read_min[-1]) @@ -257,13 +263,17 @@ def _recv(self, expected_response_length, full): else: read_min = b'' total = expected_response_length - result = self.client._recv(expected_response_length) + result = self.client.framer.recvPacket(expected_response_length) result = read_min + result actual = len(result) if actual != total: _logger.debug("Incomplete message received, " "Expected {} bytes Recieved " "{} bytes !!!!".format(total, actual)) + if self.client.state != ModbusTransactionState.PROCESSING_REPLY: + _logger.debug("Changing transaction state from " + "'WAITING FOR REPLY' to 'PROCESSING REPLY'") + self.client.state = ModbusTransactionState.PROCESSING_REPLY return result def addTransaction(self, request, tid=None): @@ -414,756 +424,9 @@ def delTransaction(self, tid): if self.transactions: self.transactions.pop(0) -#---------------------------------------------------------------------------# -# Modbus TCP Message -#---------------------------------------------------------------------------# -class ModbusSocketFramer(IModbusFramer): - ''' Modbus Socket Frame controller - - 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 ] - [ tid ][ pid ][ length ][ uid ] - 2b 2b 2b 1b 1b Nb - - while len(message) > 0: - tid, pid, length`, uid = struct.unpack(">HHHB", message) - request = message[0:7 + length - 1`] - message = [7 + length - 1:] - - * length = uid + function code + data - * The -1 is to account for the uid byte - ''' - - def __init__(self, decoder): - ''' Initializes a new instance of the framer - - :param decoder: The decoder factory implementation to use - ''' - self._buffer = b'' - self._header = {'tid':0, 'pid':0, 'len':0, 'uid':0} - self._hsize = 0x07 - self.decoder = decoder - - #-----------------------------------------------------------------------# - # Private Helper Functions - #-----------------------------------------------------------------------# - def checkFrame(self): - ''' - Check and decode the next frame Return true if we were successful - ''' - if len(self._buffer) > self._hsize: - self._header['tid'], self._header['pid'], \ - self._header['len'], self._header['uid'] = struct.unpack( - '>HHHB', self._buffer[0:self._hsize]) - - # someone sent us an error? ignore it - if self._header['len'] < 2: - self.advanceFrame() - # we have at least a complete message, continue - elif len(self._buffer) - self._hsize + 1 >= self._header['len']: - return True - # we don't have enough of a message yet, wait - return False - - def advanceFrame(self): - ''' Skip over the current framed message - This allows us to skip over the current message after we have processed - it or determined that it contains an error. It also has to reset the - current frame header handle - ''' - length = self._hsize + self._header['len'] - 1 - self._buffer = self._buffer[length:] - self._header = {'tid':0, 'pid':0, 'len':0, 'uid':0} - - def isFrameReady(self): - ''' Check if we should continue decode logic - This is meant to be used in a while loop in the decoding phase to let - the decoder factory know that there is still data in the buffer. - - :returns: True if ready, False otherwise - ''' - return len(self._buffer) > self._hsize - - def addToFrame(self, message): - ''' Adds new packet data to the current frame buffer - - :param message: The most recent packet - ''' - self._buffer += message - - def getFrame(self): - ''' Return the next frame from the buffered data - - :returns: The next full frame buffer - ''' - length = self._hsize + self._header['len'] - 1 - return self._buffer[self._hsize:length] - - def populateResult(self, result): - ''' - Populates the modbus result with the transport specific header - information (pid, tid, uid, checksum, etc) - - :param result: The response packet - ''' - result.transaction_id = self._header['tid'] - result.protocol_id = self._header['pid'] - result.unit_id = self._header['uid'] - - #-----------------------------------------------------------------------# - # Public Member Functions - #-----------------------------------------------------------------------# - def processIncomingPacket(self, data, callback): - ''' The new packet processing pattern - - 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 - messages at a time instead of 1. - - The processed and decoded messages are pushed to the callback - function to process and send. - - :param data: The new packet data - :param callback: The function to send results to - ''' - _logger.debug("Processing: "+ hexlify_packets(data)) - self.addToFrame(data) - while True: - if self.isFrameReady(): - if self.checkFrame(): - self._process(callback) - else: self.resetFrame() - else: - if len(self._buffer): - # Possible error ??? - if self._header['len'] < 2: - self._process(callback, error=True) - break - - def _process(self, callback, error=False): - """ - Process incoming packets irrespective error condition - """ - data = self.getRawFrame() if error else self.getFrame() - result = self.decoder.decode(data) - if result is None: - raise ModbusIOException("Unable to decode request") - elif error and result.function_code < 0x80: - raise InvalidMessageRecievedException(result) - else: - self.populateResult(result) - self.advanceFrame() - callback(result) # defer or push to a thread? - - def resetFrame(self): - ''' Reset the entire message frame. - This allows us to skip ovver errors that may be in the stream. - It is hard to know if we are simply out of sync or if there is - an error in the stream as we have no way to check the start or - end of the message (python just doesn't have the resolution to - check for millisecond delays). - ''' - self._buffer = b'' - self._header = {} - - def getRawFrame(self): - """ - Returns the complete buffer - """ - return self._buffer - - def buildPacket(self, message): - ''' Creates a ready to send modbus packet - - :param message: The populated request/response to send - ''' - data = message.encode() - packet = struct.pack('>HHHBB', - message.transaction_id, - message.protocol_id, - len(data) + 2, - message.unit_id, - message.function_code) + data - return packet - - -#---------------------------------------------------------------------------# -# Modbus RTU Message -#---------------------------------------------------------------------------# -class ModbusRtuFramer(IModbusFramer): - ''' - Modbus RTU Frame controller:: - - [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ] - 3.5 chars 1b 1b Nb 2b 3.5 chars - - Wait refers to the amount of time required to transmist at least x many - characters. In this case it is 3.5 characters. Also, if we recieve a - wait of 1.5 characters at any point, we must trigger an error message. - Also, it appears as though this message is little endian. The logic is - simplified as the following:: - - block-on-read: - read until 3.5 delay - check for errors - decode - - The following table is a listing of the baud wait times for the specified - baud rates:: - - ------------------------------------------------------------------ - Baud 1.5c (18 bits) 3.5c (38 bits) - ------------------------------------------------------------------ - 1200 13333.3 us 31666.7 us - 4800 3333.3 us 7916.7 us - 9600 1666.7 us 3958.3 us - 19200 833.3 us 1979.2 us - 38400 416.7 us 989.6 us - ------------------------------------------------------------------ - 1 Byte = start + 8 bits + parity + stop = 11 bits - (1/Baud)(bits) = delay seconds - ''' - - def __init__(self, decoder): - ''' Initializes a new instance of the framer - - :param decoder: The decoder factory implementation to use - ''' - self._buffer = b'' - self._header = {} - self._hsize = 0x01 - self._end = b'\x0d\x0a' - self._min_frame_size = 4 - self.decoder = decoder - - #-----------------------------------------------------------------------# - # Private Helper Functions - #-----------------------------------------------------------------------# - def checkFrame(self): - ''' - Check if the next frame is available. Return True if we were - successful. - ''' - try: - self.populateHeader() - frame_size = self._header['len'] - data = self._buffer[:frame_size - 2] - 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): - return False - - def advanceFrame(self): - ''' Skip over the current framed message - This allows us to skip over the current message after we have processed - it or determined that it contains an error. It also has to reset the - current frame header handle - ''' - try: - self._buffer = self._buffer[self._header['len']:] - except KeyError: - # Error response, no header len found - self.resetFrame() - _logger.debug("Frame advanced, resetting header!!") - self._header = {} - - def resetFrame(self): - ''' Reset the entire message frame. - This allows us to skip ovver errors that may be in the stream. - It is hard to know if we are simply out of sync or if there is - an error in the stream as we have no way to check the start or - end of the message (python just doesn't have the resolution to - check for millisecond delays). - ''' - _logger.debug("Resetting frame - Current Frame in " - "buffer - {}".format(hexlify_packets(self._buffer))) - self._buffer = b'' - self._header = {} - - def isFrameReady(self): - ''' Check if we should continue decode logic - This is meant to be used in a while loop in the decoding phase to let - the decoder know that there is still data in the buffer. - - :returns: True if ready, False otherwise - ''' - return len(self._buffer) > self._hsize - - def populateHeader(self): - ''' Try to set the headers `uid`, `len` and `crc`. - - This method examines `self._buffer` and writes meta - information into `self._header`. It calculates only the - values for headers that are not already in the dictionary. - - Beware that this method will raise an IndexError if - `self._buffer` is not yet long enough. - ''' - self._header['uid'] = byte2int(self._buffer[0]) - func_code = byte2int(self._buffer[1]) - pdu_class = self.decoder.lookupPduClass(func_code) - size = pdu_class.calculateRtuFrameSize(self._buffer) - self._header['len'] = size - self._header['crc'] = self._buffer[size - 2:size] - - def addToFrame(self, message): - ''' - This should be used before the decoding while loop to add the received - data to the buffer handle. - - :param message: The most recent packet - ''' - self._buffer += message - - def getFrame(self): - ''' Get the next frame from the buffer - - :returns: The frame data or '' - ''' - start = self._hsize - end = self._header['len'] - 2 - buffer = self._buffer[start:end] - if end > 0: - _logger.debug("Getting Frame - {}".format(hexlify_packets(buffer))) - return buffer - return b'' - - def populateResult(self, result): - ''' Populates the modbus result header - - The serial packets do not have any header information - that is copied. - - :param result: The response packet - ''' - result.unit_id = self._header['uid'] - - #-----------------------------------------------------------------------# - # Public Member Functions - #-----------------------------------------------------------------------# - def processIncomingPacket(self, data, callback): - ''' The new packet processing pattern - - 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 - messages at a time instead of 1. - - The processed and decoded messages are pushed to the callback - function to process and send. - - :param data: The new packet data - :param callback: The function to send results to - ''' - self.addToFrame(data) - while True: - if self.isFrameReady(): - if self.checkFrame(): - self._process(callback) - else: - # Could be an error response - if len(self._buffer): - # Possible error ??? - self._process(callback, error=True) - else: - if len(self._buffer): - # Possible error ??? - if self._header.get('len', 0) < 2: - self._process(callback, error=True) - break - - def buildPacket(self, message): - ''' Creates a ready to send modbus packet - - :param message: The populated request/response to send - ''' - data = message.encode() - packet = struct.pack('>BB', - message.unit_id, - message.function_code) + data - packet += struct.pack(">H", computeCRC(packet)) - return packet - - def _process(self, callback, error=False): - """ - Process incoming packets irrespective error condition - """ - data = self.getRawFrame() if error else self.getFrame() - result = self.decoder.decode(data) - if result is None: - raise ModbusIOException("Unable to decode request") - elif error and result.function_code < 0x80: - raise InvalidMessageRecievedException(result) - else: - self.populateResult(result) - self.advanceFrame() - callback(result) # defer or push to a thread? - - def getRawFrame(self): - """ - Returns the complete buffer - """ - _logger.debug("Getting Raw Frame - {}".format(self._buffer)) - return self._buffer - - - -#---------------------------------------------------------------------------# -# Modbus ASCII Message -#---------------------------------------------------------------------------# -class ModbusAsciiFramer(IModbusFramer): - ''' - Modbus ASCII Frame Controller:: - - [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] - 1c 2c 2c Nc 2c 2c - - * data can be 0 - 2x252 chars - * end is '\\r\\n' (Carriage return line feed), however the line feed - character can be changed via a special command - * start is ':' - - This framer is used for serial transmission. Unlike the RTU protocol, - the data in this framer is transferred in plain text ascii. - ''' - - def __init__(self, decoder): - ''' Initializes a new instance of the framer - - :param decoder: The decoder implementation to use - ''' - self._buffer = b'' - self._header = {'lrc':'0000', 'len':0, 'uid':0x00} - self._hsize = 0x02 - self._start = b':' - self._end = b"\r\n" - self.decoder = decoder - - #-----------------------------------------------------------------------# - # Private Helper Functions - #-----------------------------------------------------------------------# - def checkFrame(self): - ''' Check and decode the next frame - - :returns: True if we successful, False otherwise - ''' - start = self._buffer.find(self._start) - if start == -1: return False - if start > 0 : # go ahead and skip old bad data - self._buffer = self._buffer[start:] - start = 0 - - end = self._buffer.find(self._end) - if (end != -1): - self._header['len'] = end - self._header['uid'] = int(self._buffer[1:3], 16) - self._header['lrc'] = int(self._buffer[end - 2:end], 16) - data = a2b_hex(self._buffer[start + 1:end - 2]) - return checkLRC(data, self._header['lrc']) - return False - - def advanceFrame(self): - ''' Skip over the current framed message - This allows us to skip over the current message after we have processed - it or determined that it contains an error. It also has to reset the - current frame header handle - ''' - self._buffer = self._buffer[self._header['len'] + 2:] - self._header = {'lrc':'0000', 'len':0, 'uid':0x00} - - def isFrameReady(self): - ''' Check if we should continue decode logic - This is meant to be used in a while loop in the decoding phase to let - the decoder know that there is still data in the buffer. - - :returns: True if ready, False otherwise - ''' - return len(self._buffer) > 1 - - def addToFrame(self, message): - ''' Add the next message to the frame buffer - This should be used before the decoding while loop to add the received - data to the buffer handle. - - :param message: The most recent packet - ''' - self._buffer += message - - def getFrame(self): - ''' Get the next frame from the buffer - - :returns: The frame data or '' - ''' - start = self._hsize + 1 - end = self._header['len'] - 2 - buffer = self._buffer[start:end] - if end > 0: return a2b_hex(buffer) - return b'' - - def resetFrame(self): - ''' Reset the entire message frame. - This allows us to skip ovver errors that may be in the stream. - It is hard to know if we are simply out of sync or if there is - an error in the stream as we have no way to check the start or - end of the message (python just doesn't have the resolution to - check for millisecond delays). - ''' - self._buffer = b'' - self._header = {'lrc':'0000', 'len':0, 'uid':0x00} - - def populateResult(self, result): - ''' Populates the modbus result header - - The serial packets do not have any header information - that is copied. - - :param result: The response packet - ''' - result.unit_id = self._header['uid'] - - #-----------------------------------------------------------------------# - # Public Member Functions - #-----------------------------------------------------------------------# - def processIncomingPacket(self, data, callback): - ''' The new packet processing pattern - - 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 - messages at a time instead of 1. - - The processed and decoded messages are pushed to the callback - function to process and send. - - :param data: The new packet data - :param callback: The function to send results to - ''' - self.addToFrame(data) - while self.isFrameReady(): - if self.checkFrame(): - frame = self.getFrame() - result = self.decoder.decode(frame) - if result is None: - raise ModbusIOException("Unable to decode response") - self.populateResult(result) - self.advanceFrame() - callback(result) # defer this - else: - break - - def buildPacket(self, message): - ''' Creates a ready to send modbus packet - Built off of a modbus request/response - - :param message: The request/response to send - :return: The encoded packet - ''' - encoded = message.encode() - buffer = struct.pack('>BB', message.unit_id, message.function_code) - checksum = computeLRC(encoded + buffer) - - packet = bytearray() - params = (message.unit_id, message.function_code) - packet.extend(self._start) - packet.extend(('%02x%02x' % params).encode()) - packet.extend(b2a_hex(encoded)) - packet.extend(('%02x' % checksum).encode()) - packet.extend(self._end) - return bytes(packet).upper() - - -#---------------------------------------------------------------------------# -# Modbus Binary Message -#---------------------------------------------------------------------------# -class ModbusBinaryFramer(IModbusFramer): - ''' - Modbus Binary Frame Controller:: - - [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] - 1b 1b 1b Nb 2b 1b - - * data can be 0 - 2x252 chars - * end is '}' - * start is '{' - - The idea here is that we implement the RTU protocol, however, - instead of using timing for message delimiting, we use start - and end of message characters (in this case { and }). Basically, - this is a binary framer. - - The only case we have to watch out for is when a message contains - the { or } characters. If we encounter these characters, we - simply duplicate them. Hopefully we will not encounter those - characters that often and will save a little bit of bandwitch - without a real-time system. - - Protocol defined by jamod.sourceforge.net. - ''' - - def __init__(self, decoder): - ''' Initializes a new instance of the framer - - :param decoder: The decoder implementation to use - ''' - self._buffer = b'' - self._header = {'crc':0x0000, 'len':0, 'uid':0x00} - self._hsize = 0x01 - self._start = b'\x7b' # { - self._end = b'\x7d' # } - self._repeat = [b'}'[0], b'{'[0]] # python3 hack - self.decoder = decoder - - #-----------------------------------------------------------------------# - # Private Helper Functions - #-----------------------------------------------------------------------# - def checkFrame(self): - ''' Check and decode the next frame - - :returns: True if we are successful, False otherwise - ''' - start = self._buffer.find(self._start) - if start == -1: return False - if start > 0 : # go ahead and skip old bad data - self._buffer = self._buffer[start:] - - end = self._buffer.find(self._end) - if (end != -1): - self._header['len'] = end - self._header['uid'] = struct.unpack('>B', self._buffer[1:2])[0] - self._header['crc'] = struct.unpack('>H', self._buffer[end - 2:end])[0] - data = self._buffer[start + 1:end - 2] - return checkCRC(data, self._header['crc']) - return False - - def advanceFrame(self): - ''' Skip over the current framed message - This allows us to skip over the current message after we have processed - it or determined that it contains an error. It also has to reset the - current frame header handle - ''' - self._buffer = self._buffer[self._header['len'] + 2:] - self._header = {'crc':0x0000, 'len':0, 'uid':0x00} - - def isFrameReady(self): - ''' Check if we should continue decode logic - This is meant to be used in a while loop in the decoding phase to let - the decoder know that there is still data in the buffer. - - :returns: True if ready, False otherwise - ''' - return len(self._buffer) > 1 - - def addToFrame(self, message): - ''' Add the next message to the frame buffer - This should be used before the decoding while loop to add the received - data to the buffer handle. - - :param message: The most recent packet - ''' - self._buffer += message - - def getFrame(self): - ''' Get the next frame from the buffer - - :returns: The frame data or '' - ''' - start = self._hsize + 1 - end = self._header['len'] - 2 - buffer = self._buffer[start:end] - if end > 0: return buffer - return b'' - - def populateResult(self, result): - ''' Populates the modbus result header - - The serial packets do not have any header information - that is copied. - - :param result: The response packet - ''' - result.unit_id = self._header['uid'] - - #-----------------------------------------------------------------------# - # Public Member Functions - #-----------------------------------------------------------------------# - def processIncomingPacket(self, data, callback): - ''' The new packet processing pattern - - 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 - messages at a time instead of 1. - - The processed and decoded messages are pushed to the callback - function to process and send. - - :param data: The new packet data - :param callback: The function to send results to - ''' - self.addToFrame(data) - while self.isFrameReady(): - if self.checkFrame(): - result = self.decoder.decode(self.getFrame()) - if result is None: - raise ModbusIOException("Unable to decode response") - self.populateResult(result) - self.advanceFrame() - callback(result) # defer or push to a thread? - else: - break - - def buildPacket(self, message): - ''' Creates a ready to send modbus packet - - :param message: The request/response to send - :returns: The encoded packet - ''' - data = self._preflight(message.encode()) - packet = struct.pack('>BB', - message.unit_id, - message.function_code) + data - packet += struct.pack(">H", computeCRC(packet)) - packet = self._start + packet + self._end - return packet - - def _preflight(self, data): - ''' Preflight buffer test - - This basically scans the buffer for start and end - tags and if found, escapes them. - - :param data: The message to escape - :returns: the escaped packet - ''' - array = bytearray() - for d in data: - if d in self._repeat: - array.append(d) - array.append(d) - return bytes(array) - - def resetFrame(self): - ''' Reset the entire message frame. - This allows us to skip ovver errors that may be in the stream. - It is hard to know if we are simply out of sync or if there is - an error in the stream as we have no way to check the start or - end of the message (python just doesn't have the resolution to - check for millisecond delays). - ''' - self._buffer = b'' - self._header = {'crc': 0x0000, 'len': 0, 'uid': 0x00} - -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Exported symbols -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # __all__ = [ "FifoTransactionManager", "DictTransactionManager", diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index 6205829da..62967f4e0 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -8,9 +8,37 @@ from pymodbus.compat import int2byte, byte2int, IS_PYTHON3 from six import string_types + +class ModbusTransactionState(object): + """ + Modbus Client States + """ + IDLE = 0 + SENDING = 1 + WAITING_FOR_REPLY = 2 + WAITING_TURNAROUND_DELAY = 3 + PROCESSING_REPLY = 4 + PROCESSING_ERROR = 5 + TRANSCATION_COMPLETE = 6 + + @classmethod + def to_string(cls, state): + states = { + ModbusTransactionState.IDLE: "IDLE", + ModbusTransactionState.SENDING: "SENDING", + ModbusTransactionState.WAITING_FOR_REPLY: "WAITING_FOR_REPLY", + ModbusTransactionState.WAITING_TURNAROUND_DELAY: "WAITING_TURNAROUND_DELAY", + ModbusTransactionState.PROCESSING_REPLY: "PROCESSING_REPLY", + ModbusTransactionState.PROCESSING_ERROR: "PROCESSING_ERROR", + ModbusTransactionState.TRANSCATION_COMPLETE: "TRANSCATION_COMPLETE" + } + return states.get(state, None) + + # --------------------------------------------------------------------------- # # Helpers # --------------------------------------------------------------------------- # + def default(value): """ Given a python object, return the default value diff --git a/requirements.txt b/requirements.txt index 3c113cc87..6bfda9c59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ six==1.11.0 # ------------------------------------------------------------------- # if want to use the pymodbus serial stack, uncomment these # ------------------------------------------------------------------- -#pyserial==3.3 +#pyserial==3.4 # ------------------------------------------------------------------- # if you want to run the tests and code coverage, uncomment these # ------------------------------------------------------------------- diff --git a/test/test_client_sync.py b/test/test_client_sync.py index 67a2fc5cb..3c91ccd43 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -204,9 +204,9 @@ def testSyncSerialClientInstantiation(self): def testSyncSerialRTUClientTimeouts(self): client = ModbusSerialClient(method="rtu", baudrate=9600) - assert client._silent_interval == round((3.5 * 11/9600), 6) + assert client.silent_interval == round((3.5 * 11 / 9600), 6) client = ModbusSerialClient(method="rtu", baudrate=38400) - assert client._silent_interval == round((1.75/1000), 6) + assert client.silent_interval == round((1.75 / 1000), 6) @patch("serial.Serial") diff --git a/test/test_payload.py b/test/test_payload.py index fb0890754..5a43eea19 100644 --- a/test/test_payload.py +++ b/test/test_payload.py @@ -103,7 +103,7 @@ def testPayloadBuilderReset(self): def testPayloadBuilderWithRawPayload(self): """ Test basic bit message encoding/decoding """ - builder = BinaryPayloadBuilder([b'\x12', b'\x34', b'\x56', b'\x78']) + builder = BinaryPayloadBuilder([b'\x12', b'\x34', b'\x56', b'\x78'], repack=True) self.assertEqual(b'\x12\x34\x56\x78', builder.to_string()) self.assertEqual([13330, 30806], builder.to_registers()) diff --git a/test/test_server_async.py b/test/test_server_async.py index 0ed38b5d2..f4f25fbc4 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -78,7 +78,9 @@ def testDataReceived(self): mock_data = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" protocol.factory = MagicMock() protocol.factory.control.ListenOnly = False - protocol.factory.store = [byte2int(mock_data[6])] + protocol.factory.store.slaves = MagicMock() + protocol.factory.store.single = True + protocol.factory.store.slaves.return_value = [byte2int(mock_data[6])] protocol.framer = protocol._execute = MagicMock() protocol.dataReceived(mock_data) diff --git a/test/test_server_sync.py b/test/test_server_sync.py index fbb1e2df5..70a5a1dc8 100644 --- a/test/test_server_sync.py +++ b/test/test_server_sync.py @@ -108,7 +108,7 @@ def testModbusSingleRequestHandlerHandle(self): self.assertEqual(handler.framer.processIncomingPacket.call_count, 0) # run forever if we are running - def _callback1(a, b): + def _callback1(a, b, *args, **kwargs): handler.running = False # stop infinite loop handler.framer.processIncomingPacket.side_effect = _callback1 handler.running = True @@ -119,7 +119,7 @@ def _callback1(a, b): self.assertEqual(handler.framer.processIncomingPacket.call_count, 1) # exceptions are simply ignored - def _callback2(a, b): + def _callback2(a, b, *args, **kwargs): if handler.framer.processIncomingPacket.call_count == 2: raise Exception("example exception") else: handler.running = False # stop infinite loop @@ -148,6 +148,9 @@ def testModbusConnectedRequestHandlerSend(self): def testModbusConnectedRequestHandlerHandle(self): handler = socketserver.BaseRequestHandler(None, None, None) handler.__class__ = ModbusConnectedRequestHandler + handler.server = Mock() + # handler.server.context.slaves = Mock() + # protocol.factory.store.single = True handler.framer = Mock() handler.framer.buildPacket.return_value = b"message" handler.request = Mock() @@ -159,7 +162,7 @@ def testModbusConnectedRequestHandlerHandle(self): self.assertEqual(handler.framer.processIncomingPacket.call_count, 0) # run forever if we are running - def _callback(a, b): + def _callback(a, b, *args, **kwargs): handler.running = False # stop infinite loop handler.framer.processIncomingPacket.side_effect = _callback handler.running = True @@ -191,6 +194,7 @@ def testModbusDisconnectedRequestHandlerSend(self): handler = socketserver.BaseRequestHandler(None, None, None) handler.__class__ = ModbusDisconnectedRequestHandler handler.framer = Mock() + handler.server = Mock() handler.framer.buildPacket.return_value = b"message" handler.request = Mock() handler.socket = Mock() @@ -206,6 +210,7 @@ def testModbusDisconnectedRequestHandlerHandle(self): handler = socketserver.BaseRequestHandler(None, None, None) handler.__class__ = ModbusDisconnectedRequestHandler handler.framer = Mock() + handler.server = Mock() handler.framer.buildPacket.return_value = b"message" handler.request = (b"\x12\x34", handler.request) diff --git a/test/test_transaction.py b/test/test_transaction.py index 2627bd380..164dc9626 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -26,10 +26,10 @@ def setUp(self): ''' Sets up the test environment ''' self.client = None self.decoder = ServerDecoder() - self._tcp = ModbusSocketFramer(decoder=self.decoder) - self._rtu = ModbusRtuFramer(decoder=self.decoder) - self._ascii = ModbusAsciiFramer(decoder=self.decoder) - self._binary = ModbusBinaryFramer(decoder=self.decoder) + self._tcp = ModbusSocketFramer(decoder=self.decoder, client=None) + self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) + self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) + self._binary = ModbusBinaryFramer(decoder=self.decoder, client=None) self._manager = DictTransactionManager(self.client) self._queue_manager = FifoTransactionManager(self.client) self._tm = ModbusTransactionManager(self.client) @@ -318,7 +318,7 @@ def mock_callback(self): def testRTUProcessIncomingPAkcets(self): mock_data = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" - + unit = 0x00 def mock_callback(self): pass @@ -327,7 +327,7 @@ def mock_callback(self): self._rtu.isFrameReady = MagicMock(return_value=False) self._rtu._buffer = mock_data - self._rtu.processIncomingPacket(mock_data, mock_callback) + self._rtu.processIncomingPacket(mock_data, mock_callback, unit) #---------------------------------------------------------------------------# # ASCII tests @@ -390,15 +390,15 @@ def testASCIIFramerPacket(self): def testAsciiProcessIncomingPakcets(self): mock_data = msg = b':F7031389000A60\r\n' - - def mock_callback(mock_data): + unit = 0x00 + def mock_callback(mock_data, *args, **kwargs): pass - self._ascii.processIncomingPacket(mock_data, mock_callback) + self._ascii.processIncomingPacket(mock_data, mock_callback, unit) # Test failure: self._ascii.checkFrame = MagicMock(return_value=False) - self._ascii.processIncomingPacket(mock_data, mock_callback) + self._ascii.processIncomingPacket(mock_data, mock_callback, unit) #---------------------------------------------------------------------------# @@ -462,15 +462,15 @@ def testBinaryFramerPacket(self): def testBinaryProcessIncomingPacket(self): mock_data = b'\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d' - + unit = 0x00 def mock_callback(mock_data): pass - self._binary.processIncomingPacket(mock_data, mock_callback) + self._binary.processIncomingPacket(mock_data, mock_callback, unit) # Test failure: self._binary.checkFrame = MagicMock(return_value=False) - self._binary.processIncomingPacket(mock_data, mock_callback) + self._binary.processIncomingPacket(mock_data, mock_callback, unit) #---------------------------------------------------------------------------# # Main From c49038a5ade057c5491c1392f364434378a93ead Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Mon, 9 Apr 2018 09:31:00 +0530 Subject: [PATCH 4/5] 1. #277 MEI message reception issue with UDP client 2. Fix unit tests 3. Update changelog --- CHANGELOG.rst | 1 + examples/common/synchronous_server.py | 8 ++++++-- pymodbus/client/sync.py | 6 +++--- pymodbus/server/sync.py | 2 +- pymodbus/transaction.py | 3 +++ test/test_client_sync.py | 6 +++--- test/test_server_sync.py | 3 +-- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e13bfc559..e8c9bc6d4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ Version 1.5.0 * Move framers from transaction.py to respective modules * Fix modbus payload builder and decoder * Async servers can now have an option to defer `reactor.run()` when using `StartServer(...,defer_reactor_run=True)` +* Fix UDP client issue while handling MEI messages (ReadDeviceInformationRequest) * Fix Misc examples Version 1.4.0 diff --git a/examples/common/synchronous_server.py b/examples/common/synchronous_server.py index 20fc13c4f..617b1acc9 100755 --- a/examples/common/synchronous_server.py +++ b/examples/common/synchronous_server.py @@ -112,9 +112,13 @@ def run_server(): # ----------------------------------------------------------------------- # # Tcp: StartTcpServer(context, identity=identity, address=("localhost", 5020)) - + + # TCP with different framer + # StartTcpServer(context, identity=identity, + # framer=ModbusRtuFramer, address=("0.0.0.0", 5020)) + # Udp: - # StartUdpServer(context, identity=identity, address=("localhost", 5020)) + # StartUdpServer(context, identity=identity, address=("0.0.0.0", 5020)) # Ascii: # StartSerialServer(context, identity=identity, diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 213e6d22b..fb45ec6f7 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -240,7 +240,7 @@ def __str__(self): :returns: The string representation """ - return "%s:%s" % (self.host, self.port) + return "ModbusTcpClient(%s:%s)" % (self.host, self.port) # --------------------------------------------------------------------------- # @@ -330,7 +330,7 @@ def __str__(self): :returns: The string representation """ - return "%s:%s" % (self.host, self.port) + return "ModbusUdpClient(%s:%s)" % (self.host, self.port) # --------------------------------------------------------------------------- # @@ -478,7 +478,7 @@ def __str__(self): :returns: The string representation """ - return "%s baud[%s]" % (self.method, self.baudrate) + return "ModbusSerialClient(%s baud[%s])" % (self.method, self.baudrate) # --------------------------------------------------------------------------- # # Exported symbols diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index d22f76cdf..cb7c26e39 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -369,7 +369,7 @@ def __init__(self, context, framer=None, identity=None, address=None, socketserver.ThreadingUDPServer.__init__(self, self.address, self.handler) - self._BaseServer__shutdown_request = True + # self._BaseServer__shutdown_request = True def process_request(self, request, client): """ Callback for connecting a new client thread diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 3cfb4b91a..44beb8a79 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -140,6 +140,9 @@ def execute(self, request): full = True else: full = False + c_str = str(self.client) + if "modbusudpclient" in c_str.lower().strip(): + full = True response, last_exception = self._transact(request, expected_response_length, full=full diff --git a/test/test_client_sync.py b/test/test_client_sync.py index 3c91ccd43..d503d0dda 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -91,7 +91,7 @@ def testBasicSyncUdpClient(self): client.socket = False client.close() - self.assertEqual("127.0.0.1:502", str(client)) + self.assertEqual("ModbusUdpClient(127.0.0.1:502)", str(client)) def testUdpClientAddressFamily(self): ''' Test the Udp client get address family method''' @@ -158,7 +158,7 @@ def testBasicSyncTcpClient(self): client.socket = False client.close() - self.assertEqual("127.0.0.1:502", str(client)) + self.assertEqual("ModbusTcpClient(127.0.0.1:502)", str(client)) def testTcpClientConnect(self): ''' Test the tcp client connection method''' @@ -234,7 +234,7 @@ def testBasicSyncSerialClient(self, mock_serial): client.socket = False client.close() - self.assertEqual('ascii baud[19200]', str(client)) + self.assertEqual('ModbusSerialClient(ascii baud[19200])', str(client)) def testSerialClientConnect(self): ''' Test the serial client connection method''' diff --git a/test/test_server_sync.py b/test/test_server_sync.py index 70a5a1dc8..38f1121aa 100644 --- a/test/test_server_sync.py +++ b/test/test_server_sync.py @@ -29,7 +29,7 @@ #---------------------------------------------------------------------------# class MockServer(object): def __init__(self): - self.framer = lambda _: "framer" + self.framer = lambda _, client=None: "framer" self.decoder = "decoder" self.threads = [] self.context = {} @@ -59,7 +59,6 @@ def testBaseHandlerMethods(self): request = ReadCoilsRequest(1, 1) address = ('server', 12345) server = MockServer() - with patch.object(ModbusBaseRequestHandler, 'handle') as mock_handle: with patch.object(ModbusBaseRequestHandler, 'send') as mock_send: mock_handle.return_value = True From 74782b22a28a399f8fc92b18c34013cd926a91b5 Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Thu, 26 Apr 2018 18:25:00 +0530 Subject: [PATCH 5/5] Patch 1 (#292) * Fix #289 and other misc enhancements * Replace nosetest with pytest * Update Changelog * serial sync client wait till timeout/some data is available in read buffer + update changelog * serial sync client read updates when timeout is None and Zero * fix sync client unit test and example --- CHANGELOG.rst | 6 +- Makefile | 2 +- examples/common/synchronous_client.py | 3 +- examples/common/synchronous_client_ext.py | 4 +- pymodbus/bit_write_message.py | 7 ++ pymodbus/client/sync.py | 65 +++++++++-- pymodbus/constants.py | 2 +- pymodbus/exceptions.py | 2 +- pymodbus/factory.py | 12 +- pymodbus/framer/rtu_framer.py | 40 ++----- pymodbus/framer/socket_framer.py | 4 +- pymodbus/other_message.py | 2 +- pymodbus/register_write_message.py | 7 ++ pymodbus/transaction.py | 35 +++--- pymodbus/utilities.py | 4 +- requirements-tests.txt | 2 + setup.cfg | 6 +- test/test_client_sync.py | 132 ++++++++++++++-------- test/test_transaction.py | 2 +- 19 files changed, 227 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e8c9bc6d4..e150c61f1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,7 +10,11 @@ Version 1.5.0 * Move framers from transaction.py to respective modules * Fix modbus payload builder and decoder * Async servers can now have an option to defer `reactor.run()` when using `StartServer(...,defer_reactor_run=True)` -* Fix UDP client issue while handling MEI messages (ReadDeviceInformationRequest) +* Fix UDP client issue while handling MEI messages (ReadDeviceInformationRequest) +* Add expected response lengths for WriteMultipleCoilRequest and WriteMultipleRegisterRequest +* Fix struct errors while decoding stray response +* Modbus read retries works only when empty/no message is received +* Change test runner from nosetest to pytest * Fix Misc examples Version 1.4.0 diff --git a/Makefile b/Makefile index 7022b2807..43d6e43ae 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ check: install test: install @pip install --quiet --requirement=requirements-tests.txt - @nosetests --with-coverage --cover-html + @py.test @coverage report --fail-under=90 tox: install diff --git a/examples/common/synchronous_client.py b/examples/common/synchronous_client.py index 41578e1fc..f7186dd0f 100755 --- a/examples/common/synchronous_client.py +++ b/examples/common/synchronous_client.py @@ -17,7 +17,7 @@ # import the various server implementations # --------------------------------------------------------------------------- # from pymodbus.client.sync import ModbusTcpClient as ModbusClient -#from pymodbus.client.sync import ModbusUdpClient as ModbusClient +# from pymodbus.client.sync import ModbusUdpClient as ModbusClient # from pymodbus.client.sync import ModbusSerialClient as ModbusClient # --------------------------------------------------------------------------- # @@ -62,6 +62,7 @@ def run_sync_client(): # client = ModbusClient('localhost', retries=3, retry_on_empty=True) # ------------------------------------------------------------------------# client = ModbusClient('localhost', port=5020) + # from pymodbus.transaction import ModbusRtuFramer # client = ModbusClient('localhost', port=5020, framer=ModbusRtuFramer) # client = ModbusClient(method='binary', port='/dev/ptyp0', timeout=1) # client = ModbusClient(method='ascii', port='/dev/ptyp0', timeout=1) diff --git a/examples/common/synchronous_client_ext.py b/examples/common/synchronous_client_ext.py index 61d42ff0a..a7a8a83d6 100755 --- a/examples/common/synchronous_client_ext.py +++ b/examples/common/synchronous_client_ext.py @@ -13,7 +13,7 @@ # from pymodbus.client.sync import ModbusTcpClient as ModbusClient # from pymodbus.client.sync import ModbusUdpClient as ModbusClient from pymodbus.client.sync import ModbusSerialClient as ModbusClient -from pymodbus.transaction import ModbusRtuFramer + # --------------------------------------------------------------------------- # # import the extended messages to perform @@ -51,6 +51,8 @@ def execute_extended_requests(): # client = ModbusClient(method='ascii', port="/dev/ptyp0") # client = ModbusClient(method='binary', port="/dev/ptyp0") # client = ModbusClient('127.0.0.1', port=5020) + # from pymodbus.transaction import ModbusRtuFramer + # client = ModbusClient('127.0.0.1', port=5020, framer=ModbusRtuFramer) client.connect() # ----------------------------------------------------------------------- # diff --git a/pymodbus/bit_write_message.py b/pymodbus/bit_write_message.py index 641c1cf31..2dfb61892 100644 --- a/pymodbus/bit_write_message.py +++ b/pymodbus/bit_write_message.py @@ -214,6 +214,13 @@ def __str__(self): params = (self.address, len(self.values)) return "WriteNCoilRequest (%d) => %d " % params + def get_response_pdu_size(self): + """ + Func_code (1 byte) + Output Address (2 byte) + Quantity of Outputs (2 Bytes) + :return: + """ + return 1 + 2 + 2 + class WriteMultipleCoilsResponse(ModbusResponse): ''' diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index fb45ec6f7..f1642aee7 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -2,7 +2,7 @@ import serial import time import sys - +from functools import partial from pymodbus.constants import Defaults from pymodbus.utilities import hexlify_packets, ModbusTransactionState from pymodbus.factory import ClientDecoder @@ -230,7 +230,35 @@ def _recv(self, size): """ if not self.socket: raise ConnectionException(self.__str__()) - return self.socket.recv(size) + # socket.recv(size) waits until it gets some data from the host but + # not necessarily the entire response that can be fragmented in + # many packets. + # To avoid the splitted responses to be recognized as invalid + # messages and to be discarded, loops socket.recv until full data + # is received or timeout is expired. + # If timeout expires returns the read data, also if its length is + # less than the expected size. + self.socket.setblocking(0) + begin = time.time() + + data = b'' + if size is not None: + while len(data) < size: + try: + data += self.socket.recv(size - len(data)) + except socket.error: + pass + if not self.timeout or (time.time() - begin > self.timeout): + break + else: + while True: + try: + data += self.socket.recv(1) + except socket.error: + pass + if not self.timeout or (time.time() - begin > self.timeout): + break + return data def is_socket_open(self): return True if self.socket is not None else False @@ -423,6 +451,16 @@ def close(self): self.socket.close() self.socket = None + def _in_waiting(self): + in_waiting = ("in_waiting" if hasattr( + self.socket, "in_waiting") else "inWaiting") + + if in_waiting == "in_waiting": + waitingbytes = getattr(self.socket, in_waiting) + else: + waitingbytes = getattr(self.socket, in_waiting)() + return waitingbytes + def _send(self, request): """ Sends data on the underlying socket @@ -438,13 +476,7 @@ def _send(self, request): raise ConnectionException(self.__str__()) if request: try: - in_waiting = ("in_waiting" if hasattr( - self.socket, "in_waiting") else "inWaiting") - - if in_waiting == "in_waiting": - waitingbytes = getattr(self.socket, in_waiting) - else: - waitingbytes = getattr(self.socket, in_waiting)() + waitingbytes = self._in_waiting() if waitingbytes: result = self.socket.read(waitingbytes) if _logger.isEnabledFor(logging.WARNING): @@ -457,6 +489,19 @@ def _send(self, request): return size 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) + else: + condition = partial(lambda dummy1, dummy2: True, dummy2=None) + start = time.time() + while condition(start): + size = self._in_waiting() + if size: + break + time.sleep(0.01) + return size + def _recv(self, size): """ Reads data from the underlying descriptor @@ -465,6 +510,8 @@ def _recv(self, size): """ if not self.socket: raise ConnectionException(self.__str__()) + if size is None: + size = self._wait_for_data() result = self.socket.read(size) return result diff --git a/pymodbus/constants.py b/pymodbus/constants.py index 45b722e19..5763c77d7 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -103,7 +103,7 @@ class Defaults(Singleton): Stopbits = 1 ZeroMode = False IgnoreMissingSlaves = False - + ReadSize = 1024 class ModbusStatus(Singleton): ''' diff --git a/pymodbus/exceptions.py b/pymodbus/exceptions.py index a2ad48241..b225a4dd6 100644 --- a/pymodbus/exceptions.py +++ b/pymodbus/exceptions.py @@ -78,7 +78,7 @@ def __init__(self, string=""): ModbusException.__init__(self, message) -class InvalidMessageRecievedException(ModbusException): +class InvalidMessageReceivedException(ModbusException): """ Error resulting from invalid response received or decoded """ diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 7b99fe1d6..37f3eb491 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -223,6 +223,9 @@ def decode(self, message): return self._helper(message) except ModbusException as er: _logger.error("Unable to decode response %s" % er) + + except Exception as ex: + _logger.error(ex) return None def _helper(self, data): @@ -234,8 +237,13 @@ def _helper(self, data): :param data: The response packet to decode :returns: The decoded request or an exception response object ''' - function_code = byte2int(data[0]) - _logger.debug("Factory Response[%d]" % function_code) + fc_string = function_code = byte2int(data[0]) + if function_code in self.__lookup: + fc_string = "%s: %s" % ( + str(self.__lookup[function_code]).split('.')[-1].rstrip("'>"), + function_code + ) + _logger.debug("Factory Response[%s]" % fc_string) response = self.__lookup.get(function_code, lambda: None)() if function_code > 0x80: code = function_code & 0x7f # strip error portion diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py index b39649ea4..21c932842 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/rtu_framer.py @@ -2,7 +2,7 @@ import time from pymodbus.exceptions import ModbusIOException -from pymodbus.exceptions import InvalidMessageRecievedException +from pymodbus.exceptions import InvalidMessageReceivedException from pymodbus.utilities import checkCRC, computeCRC from pymodbus.utilities import hexlify_packets, ModbusTransactionState from pymodbus.compat import byte2int @@ -213,27 +213,16 @@ def processIncomingPacket(self, data, callback, unit, **kwargs): unit = [unit] self.addToFrame(data) single = kwargs.get("single", False) - while True: - if self.isFrameReady(): - if self.checkFrame(): - if self._validate_unit_id(unit, single): - self._process(callback) - else: - _logger.debug("Not a valid unit id - {}, " - "ignoring!!".format(self._header['uid'])) - self.resetFrame() - + if self.isFrameReady(): + if self.checkFrame(): + if self._validate_unit_id(unit, single): + self._process(callback) else: - # Could be an error response - if len(self._buffer): - # Possible error ??? - self._process(callback, error=True) - else: - if len(self._buffer): - # Possible error ??? - if self._header.get('len', 0) < 2: - self._process(callback, error=True) - break + _logger.debug("Not a valid unit id - {}, " + "ignoring!!".format(self._header['uid'])) + self.resetFrame() + else: + _logger.debug("Frame - [{}] not ready".format(data)) def buildPacket(self, message): """ @@ -258,7 +247,7 @@ def sendPacket(self, message): # ModbusTransactionState.to_string(self.client.state)) # ) while self.client.state != ModbusTransactionState.IDLE: - if self.client.state == ModbusTransactionState.TRANSCATION_COMPLETE: + if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE: ts = round(time.time(), 6) _logger.debug("Changing state to IDLE - Last Frame End - {}, " "Current Time stamp - {}".format( @@ -296,11 +285,6 @@ def recvPacket(self, size): :return: """ result = self.client.recv(size) - # if self.client.state != ModbusTransactionState.PROCESSING_REPLY: - # _logger.debug("Changing transaction state from " - # "'WAITING FOR REPLY' to 'PROCESSING REPLY'") - # self.client.state = ModbusTransactionState.PROCESSING_REPLY - self.client.last_frame_end = round(time.time(), 6) return result @@ -313,7 +297,7 @@ def _process(self, callback, error=False): if result is None: raise ModbusIOException("Unable to decode request") elif error and result.function_code < 0x80: - raise InvalidMessageRecievedException(result) + raise InvalidMessageReceivedException(result) else: self.populateResult(result) self.advanceFrame() diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/socket_framer.py index 37e3bfe9d..201018960 100644 --- a/pymodbus/framer/socket_framer.py +++ b/pymodbus/framer/socket_framer.py @@ -1,6 +1,6 @@ import struct from pymodbus.exceptions import ModbusIOException -from pymodbus.exceptions import InvalidMessageRecievedException +from pymodbus.exceptions import InvalidMessageReceivedException from pymodbus.utilities import hexlify_packets from pymodbus.framer import ModbusFramer, SOCKET_FRAME_HEADER @@ -174,7 +174,7 @@ def _process(self, callback, error=False): if result is None: raise ModbusIOException("Unable to decode request") elif error and result.function_code < 0x80: - raise InvalidMessageRecievedException(result) + raise InvalidMessageReceivedException(result) else: self.populateResult(result) self.advanceFrame() diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index b30ea7598..bf7c991c6 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -278,7 +278,7 @@ class GetCommEventLogResponse(ModbusResponse): field defines the total length of the data in these four field ''' function_code = 0x0c - _rtu_byte_count_pos = 3 + _rtu_byte_count_pos = 2 def __init__(self, **kwargs): ''' Initializes a new instance diff --git a/pymodbus/register_write_message.py b/pymodbus/register_write_message.py index 06e229319..3a128aba7 100644 --- a/pymodbus/register_write_message.py +++ b/pymodbus/register_write_message.py @@ -192,6 +192,13 @@ def execute(self, context): context.setValues(self.function_code, self.address, self.values) return WriteMultipleRegistersResponse(self.address, self.count) + def get_response_pdu_size(self): + """ + Func_code (1 byte) + Starting Address (2 byte) + Quantity of Reggisters (2 Bytes) + :return: + """ + return 1 + 2 + 2 + def __str__(self): ''' Returns a string representation of the instance diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 44beb8a79..6f73c158d 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -4,9 +4,10 @@ import struct import socket from threading import RLock +from functools import partial from pymodbus.exceptions import ModbusIOException, NotImplementedException -from pymodbus.exceptions import InvalidMessageRecievedException +from pymodbus.exceptions import InvalidMessageReceivedException from pymodbus.constants import Defaults from pymodbus.framer.ascii_framer import ModbusAsciiFramer from pymodbus.framer.rtu_framer import ModbusRtuFramer @@ -143,6 +144,8 @@ def execute(self, request): c_str = str(self.client) if "modbusudpclient" in c_str.lower().strip(): full = True + if not expected_response_length: + expected_response_length = 1024 response, last_exception = self._transact(request, expected_response_length, full=full @@ -167,25 +170,30 @@ def execute(self, request): retries -= 1 continue break + addTransaction = partial(self.addTransaction, + tid=request.transaction_id) self.client.framer.processIncomingPacket(response, - self.addTransaction, + addTransaction, request.unit_id) response = self.getTransaction(request.transaction_id) if not response: if len(self.transactions): response = self.getTransaction(tid=0) else: - last_exception = last_exception or ("No Response received " - "from the remote unit") + last_exception = last_exception or ( + "No Response received from the remote unit" + "/Unable to decode response") response = ModbusIOException(last_exception) if hasattr(self.client, "state"): _logger.debug("Changing transaction state from " - "'PROCESSING REPLY' to 'TRANSCATION_COMPLETE'") - self.client.state = ModbusTransactionState.TRANSCATION_COMPLETE + "'PROCESSING REPLY' to " + "'TRANSACTION_COMPLETE'") + self.client.state = ( + ModbusTransactionState.TRANSACTION_COMPLETE) return response except Exception as ex: _logger.exception(ex) - self.client.state = ModbusTransactionState.TRANSCATION_COMPLETE + self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE raise def _transact(self, packet, response_length, full=False): @@ -208,11 +216,11 @@ def _transact(self, packet, response_length, full=False): _logger.debug("Changing transaction state from 'SENDING' " "to 'WAITING FOR REPLY'") self.client.state = ModbusTransactionState.WAITING_FOR_REPLY - result = self._recv(response_length or 1024, full) + result = self._recv(response_length, full) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("RECV: " + hexlify_packets(result)) except (socket.error, ModbusIOException, - InvalidMessageRecievedException) as msg: + InvalidMessageReceivedException) as msg: self.client.close() _logger.debug("Transaction failed. (%s) " % msg) last_exception = msg @@ -223,7 +231,7 @@ def _send(self, packet): return self.client.framer.sendPacket(packet) def _recv(self, expected_response_length, full): - expected_response_length = expected_response_length or 1024 + total = None if not full: exception_length = self._calculate_exception_length() if isinstance(self.client.framer, ModbusSocketFramer): @@ -256,8 +264,9 @@ def _recv(self, expected_response_length, full): h_size = self.client.framer._hsize length = struct.unpack(">H", read_min[4:6])[0] - 1 expected_response_length = h_size + length - expected_response_length -= min_size - total = expected_response_length + min_size + if expected_response_length is not None: + expected_response_length -= min_size + total = expected_response_length + min_size else: expected_response_length = exception_length - min_size total = expected_response_length + min_size @@ -269,7 +278,7 @@ def _recv(self, expected_response_length, full): result = self.client.framer.recvPacket(expected_response_length) result = read_min + result actual = len(result) - if actual != total: + if total is not None and actual != total: _logger.debug("Incomplete message received, " "Expected {} bytes Recieved " "{} bytes !!!!".format(total, actual)) diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index 62967f4e0..dff3f10b2 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -19,7 +19,7 @@ class ModbusTransactionState(object): WAITING_TURNAROUND_DELAY = 3 PROCESSING_REPLY = 4 PROCESSING_ERROR = 5 - TRANSCATION_COMPLETE = 6 + TRANSACTION_COMPLETE = 6 @classmethod def to_string(cls, state): @@ -30,7 +30,7 @@ def to_string(cls, state): ModbusTransactionState.WAITING_TURNAROUND_DELAY: "WAITING_TURNAROUND_DELAY", ModbusTransactionState.PROCESSING_REPLY: "PROCESSING_REPLY", ModbusTransactionState.PROCESSING_ERROR: "PROCESSING_ERROR", - ModbusTransactionState.TRANSCATION_COMPLETE: "TRANSCATION_COMPLETE" + ModbusTransactionState.TRANSACTION_COMPLETE: "TRANSCATION_COMPLETE" } return states.get(state, None) diff --git a/requirements-tests.txt b/requirements-tests.txt index 5c4639d1b..044b5a879 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -9,6 +9,8 @@ zope.interface>=4.4.0 pyasn1>=0.2.3 pycrypto>=2.6.1 pyserial>=3.4 +pytest-cov>=2.5.1 +pytest>=3.5.0 redis>=2.10.5 sqlalchemy>=1.1.15 #wsgiref>=0.1.2 diff --git a/setup.cfg b/setup.cfg index 219c63c5d..9e8519e74 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,4 +26,8 @@ all_files = 1 upload-dir = build/sphinx/html [bdist_wheel] -universal=1 \ No newline at end of file +universal=1 + +[tool:pytest] +addopts = --cov=pymodbus/ +testpaths = test \ No newline at end of file diff --git a/test/test_client_sync.py b/test/test_client_sync.py index d503d0dda..0c33781b7 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -1,10 +1,11 @@ #!/usr/bin/env python import unittest from pymodbus.compat import IS_PYTHON3 -if IS_PYTHON3: # Python 3 - from unittest.mock import patch, Mock -else: # Python 2 - from mock import patch, Mock + +if IS_PYTHON3: # Python 3 + from unittest.mock import patch, Mock, MagicMock +else: # Python 2 + from mock import patch, Mock, MagicMock import socket import serial @@ -15,30 +16,43 @@ from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer from pymodbus.transaction import ModbusBinaryFramer -#---------------------------------------------------------------------------# + +# ---------------------------------------------------------------------------# # Mock Classes -#---------------------------------------------------------------------------# +# ---------------------------------------------------------------------------# class mockSocket(object): + timeout = 2 def close(self): return True - def recv(self, size): return '\x00'*size - def read(self, size): return '\x00'*size + + def recv(self, size): return b'\x00' * size + + def read(self, size): return b'\x00' * size + def send(self, msg): return len(msg) + def write(self, msg): return len(msg) - def recvfrom(self, size): return ['\x00'*size] + + def recvfrom(self, size): return [b'\x00' * size] + def sendto(self, msg, *args): return len(msg) + + def setblocking(self, flag): return None + def in_waiting(self): return None -#---------------------------------------------------------------------------# + + +# ---------------------------------------------------------------------------# # Fixture -#---------------------------------------------------------------------------# +# ---------------------------------------------------------------------------# class SynchronousClientTest(unittest.TestCase): ''' This is the unittest for the pymodbus.client.sync module ''' - #-----------------------------------------------------------------------# + # -----------------------------------------------------------------------# # Test Base Client - #-----------------------------------------------------------------------# + # -----------------------------------------------------------------------# def testBaseModbusClient(self): ''' Test the base class for all the clients ''' @@ -50,9 +64,10 @@ def testBaseModbusClient(self): self.assertRaises(NotImplementedException, lambda: client._recv(None)) self.assertRaises(NotImplementedException, lambda: client.__enter__()) self.assertRaises(NotImplementedException, lambda: client.execute()) + self.assertRaises(NotImplementedException, lambda: client.is_socket_open()) self.assertEqual("Null Transport", str(client)) client.close() - client.__exit__(0,0,0) + client.__exit__(0, 0, 0) # a successful execute client.connect = lambda: True @@ -65,9 +80,9 @@ def testBaseModbusClient(self): self.assertRaises(ConnectionException, lambda: client.__enter__()) self.assertRaises(ConnectionException, lambda: client.execute()) - #-----------------------------------------------------------------------# + # -----------------------------------------------------------------------# # Test UDP Client - #-----------------------------------------------------------------------# + # -----------------------------------------------------------------------# def testSyncUdpClientInstantiation(self): client = ModbusUdpClient() @@ -80,8 +95,8 @@ def testBasicSyncUdpClient(self): client = ModbusUdpClient() client.socket = mockSocket() self.assertEqual(0, client._send(None)) - self.assertEqual(1, client._send('\x00')) - self.assertEqual('\x00', client._recv(1)) + self.assertEqual(1, client._send(b'\x00')) + self.assertEqual(b'\x00', client._recv(1)) # connect/disconnect self.assertTrue(client.connect()) @@ -96,7 +111,8 @@ def testBasicSyncUdpClient(self): def testUdpClientAddressFamily(self): ''' Test the Udp client get address family method''' client = ModbusUdpClient() - self.assertEqual(socket.AF_INET, client._get_address_family('127.0.0.1')) + self.assertEqual(socket.AF_INET, + client._get_address_family('127.0.0.1')) self.assertEqual(socket.AF_INET6, client._get_address_family('::1')) def testUdpClientConnect(self): @@ -105,6 +121,7 @@ def testUdpClientConnect(self): class DummySocket(object): def settimeout(self, *a, **kwa): pass + mock_method.return_value = DummySocket() client = ModbusUdpClient() self.assertTrue(client.connect()) @@ -129,13 +146,14 @@ def testUdpClientRecv(self): self.assertRaises(ConnectionException, lambda: client._recv(1024)) client.socket = mockSocket() - self.assertEqual('', client._recv(0)) - self.assertEqual('\x00'*4, client._recv(4)) + self.assertEqual(b'', client._recv(0)) + self.assertEqual(b'\x00' * 4, client._recv(4)) + - #-----------------------------------------------------------------------# + # -----------------------------------------------------------------------# # Test TCP Client - #-----------------------------------------------------------------------# - + # -----------------------------------------------------------------------# + def testSyncTcpClientInstantiation(self): client = ModbusTcpClient() self.assertNotEqual(client, None) @@ -147,8 +165,8 @@ def testBasicSyncTcpClient(self): client = ModbusTcpClient() client.socket = mockSocket() self.assertEqual(0, client._send(None)) - self.assertEqual(1, client._send('\x00')) - self.assertEqual('\x00', client._recv(1)) + self.assertEqual(1, client._send(b'\x00')) + self.assertEqual(b'\x00', client._recv(1)) # connect/disconnect self.assertTrue(client.connect()) @@ -187,20 +205,39 @@ def testTcpClientRecv(self): self.assertRaises(ConnectionException, lambda: client._recv(1024)) client.socket = mockSocket() - self.assertEqual('', client._recv(0)) - self.assertEqual('\x00'*4, client._recv(4)) - - #-----------------------------------------------------------------------# + self.assertEqual(b'', client._recv(0)) + self.assertEqual(b'\x00' * 4, client._recv(4)) + + mock_socket = MagicMock() + mock_socket.recv.side_effect = iter([b'\x00', b'\x01', b'\x02']) + client.socket = mock_socket + client.timeout = 1 + self.assertEqual(b'\x00\x01\x02', client._recv(3)) + mock_socket.recv.side_effect = iter([b'\x00', b'\x01', b'\x02']) + self.assertEqual(b'\x00\x01', client._recv(2)) + mock_socket.recv.side_effect = socket.error('No data') + self.assertEqual(b'', client._recv(2)) + client.socket = mockSocket() + client.socket.timeout = 0.1 + self.assertIn(b'\x00', client._recv(None)) + + + + # -----------------------------------------------------------------------# # Test Serial Client - #-----------------------------------------------------------------------# + # -----------------------------------------------------------------------# def testSyncSerialClientInstantiation(self): client = ModbusSerialClient() self.assertNotEqual(client, None) - self.assertTrue(isinstance(ModbusSerialClient(method='ascii').framer, ModbusAsciiFramer)) - self.assertTrue(isinstance(ModbusSerialClient(method='rtu').framer, ModbusRtuFramer)) - self.assertTrue(isinstance(ModbusSerialClient(method='binary').framer, ModbusBinaryFramer)) - self.assertRaises(ParameterException, lambda: ModbusSerialClient(method='something')) + self.assertTrue(isinstance(ModbusSerialClient(method='ascii').framer, + ModbusAsciiFramer)) + self.assertTrue(isinstance(ModbusSerialClient(method='rtu').framer, + ModbusRtuFramer)) + self.assertTrue(isinstance(ModbusSerialClient(method='binary').framer, + ModbusBinaryFramer)) + self.assertRaises(ParameterException, + lambda: ModbusSerialClient(method='something')) def testSyncSerialRTUClientTimeouts(self): client = ModbusSerialClient(method="rtu", baudrate=9600) @@ -208,7 +245,6 @@ def testSyncSerialRTUClientTimeouts(self): client = ModbusSerialClient(method="rtu", baudrate=38400) assert client.silent_interval == round((1.75 / 1000), 6) - @patch("serial.Serial") def testBasicSyncSerialClient(self, mock_serial): ''' Test the basic methods for the serial sync client''' @@ -217,14 +253,14 @@ def testBasicSyncSerialClient(self, mock_serial): mock_serial.in_waiting = 0 mock_serial.write = lambda x: len(x) - mock_serial.read = lambda size: '\x00' * size + mock_serial.read = lambda size: b'\x00' * size client = ModbusSerialClient() client.socket = mock_serial client.state = 0 self.assertEqual(0, client._send(None)) client.state = 0 - self.assertEqual(1, client._send('\x00')) - self.assertEqual('\x00', client._recv(1)) + self.assertEqual(1, client._send(b'\x00')) + self.assertEqual(b'\x00', client._recv(1)) # connect/disconnect self.assertTrue(client.connect()) @@ -266,7 +302,7 @@ def testSerialClientSend(self, mock_serial): def testSerialClientCleanupBufferBeforeSend(self, mock_serial): ''' Test the serial client send method''' mock_serial.in_waiting = 4 - mock_serial.read = lambda x: b'1'*x + mock_serial.read = lambda x: b'1' * x mock_serial.write = lambda x: len(x) client = ModbusSerialClient() self.assertRaises(ConnectionException, lambda: client._send(None)) @@ -283,11 +319,17 @@ def testSerialClientRecv(self): self.assertRaises(ConnectionException, lambda: client._recv(1024)) client.socket = mockSocket() - self.assertEqual('', client._recv(0)) - self.assertEqual('\x00'*4, client._recv(4)) + self.assertEqual(b'', client._recv(0)) + self.assertEqual(b'\x00' * 4, client._recv(4)) + client.socket = MagicMock() + client.socket.read.return_value = b'' + self.assertEqual(b'', client._recv(None)) + client.socket.timeout = 0 + self.assertEqual(b'', client._recv(0)) + -#---------------------------------------------------------------------------# +# ---------------------------------------------------------------------------# # Main -#---------------------------------------------------------------------------# +# ---------------------------------------------------------------------------# if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file diff --git a/test/test_transaction.py b/test/test_transaction.py index 164dc9626..7ab576dbf 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -11,7 +11,7 @@ from pymodbus.compat import byte2int from mock import MagicMock from pymodbus.exceptions import ( - NotImplementedException, ModbusIOException, InvalidMessageRecievedException + NotImplementedException, ModbusIOException, InvalidMessageReceivedException ) class ModbusTransactionTest(unittest.TestCase):