diff --git a/README.rst b/README.rst index 2fc5eee42..9131d6379 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,14 @@ .. image:: https://travis-ci.org/riptideio/pymodbus.svg?branch=master :target: https://travis-ci.org/riptideio/pymodbus - .. image:: https://badges.gitter.im/Join%20Chat.svg - :target: https://gitter.im/pymodbus_dev/Lobby - + :target: https://gitter.im/pymodbus_dev/Lobby .. image:: https://readthedocs.org/projects/pymodbus/badge/?version=latest - :target: http://pymodbus.readthedocs.io/en/async/?badge=latest + :target: http://pymodbus.readthedocs.io/en/async/?badge=latest :alt: Documentation Status - +.. image:: http://pepy.tech/badge/pymodbus + :target: http://pepy.tech/project/pymodbus + :alt: Downloads + .. important:: **Note This is a Major release and might affect your existing Async client implementation. Refer examples on how to use the latest async clients.** diff --git a/examples/common/synchronous_server.py b/examples/common/synchronous_server.py index 617b1acc9..233adaa0d 100755 --- a/examples/common/synchronous_server.py +++ b/examples/common/synchronous_server.py @@ -6,11 +6,11 @@ The synchronous server is implemented in pure python without any third party libraries (unless you need to use the serial protocols which require pyserial). This is helpful in constrained or old environments where using -twisted just is not feasable. What follows is an examle of its use: +twisted is just not feasible. What follows is an example of its use: """ -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # import the various server implementations -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # from pymodbus.server.sync import StartTcpServer from pymodbus.server.sync import StartUdpServer from pymodbus.server.sync import StartSerialServer @@ -20,9 +20,9 @@ from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext from pymodbus.transaction import ModbusRtuFramer, ModbusBinaryFramer -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # configure the service logging -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # import logging FORMAT = ('%(asctime)-15s %(threadName)-15s' ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s') @@ -32,9 +32,9 @@ def run_server(): - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # # initialize your data store - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # # The datastores only respond to the addresses that they are initialized to # Therefore, if you initialize a DataBlock to addresses of 0x00 to 0xFF, a # request to 0x100 will respond with an invalid address exception. This is @@ -58,7 +58,7 @@ def run_server(): # store = ModbusSlaveContext() # # Finally, you are allowed to use the same DataBlock reference for every - # table or you you may use a seperate DataBlock for each table. + # table or you may use a separate DataBlock for each table. # This depends if you would like functions to be able to access and modify # the same data or not:: # @@ -85,7 +85,7 @@ def run_server(): # will map to (1-8):: # # store = ModbusSlaveContext(..., zero_mode=True) - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # store = ModbusSlaveContext( di=ModbusSequentialDataBlock(0, [17]*100), co=ModbusSequentialDataBlock(0, [17]*100), @@ -93,12 +93,12 @@ def run_server(): ir=ModbusSequentialDataBlock(0, [17]*100)) context = ModbusServerContext(slaves=store, single=True) - - # ----------------------------------------------------------------------- # + + # ----------------------------------------------------------------------- # # initialize the server information - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # # If you don't set this or any fields, they are defaulted to empty strings. - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # identity = ModbusDeviceIdentification() identity.VendorName = 'Pymodbus' identity.ProductCode = 'PM' @@ -109,7 +109,7 @@ def run_server(): # ----------------------------------------------------------------------- # # run the server you want - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # # Tcp: StartTcpServer(context, identity=identity, address=("localhost", 5020)) @@ -119,11 +119,11 @@ def run_server(): # Udp: # StartUdpServer(context, identity=identity, address=("0.0.0.0", 5020)) - + # Ascii: # StartSerialServer(context, identity=identity, # port='/dev/ttyp0', timeout=1) - + # RTU: # StartSerialServer(context, framer=ModbusRtuFramer, identity=identity, # port='/dev/ttyp0', timeout=.005, baudrate=9600) diff --git a/examples/contrib/remote_server_context.py b/examples/contrib/remote_server_context.py index b8af75e4a..7cc62ae5f 100644 --- a/examples/contrib/remote_server_context.py +++ b/examples/contrib/remote_server_context.py @@ -73,7 +73,7 @@ def validate(self, fx, address, count=1): return not result.isError() def getValues(self, fx, address, count=1): - """ Validates the request to make sure it is in range + """ Get `count` values from datastore :param fx: The function we are working with :param address: The starting address @@ -118,7 +118,8 @@ def __extract_result(self, fx, result): return result.bits if fx in ['h', 'i']: return result.registers - else: return result + else: + return result # -------------------------------------------------------------------------- # # Server Context @@ -152,7 +153,7 @@ def __init__(self, client): 'i': lambda a, v, s: client.write_registers(a, v, s), } self._client = client - self.slaves = {} # simply a cache + self.slaves = {} # simply a cache def __str__(self): """ Returns a string representation of the context @@ -187,14 +188,14 @@ def __setitem__(self, slave, context): :param slave: The slave context to set :param context: The new context to set for this slave """ - raise NotImplementedException() # doesn't make sense here + raise NotImplementedException() # doesn't make sense here def __delitem__(self, slave): """ Wrapper used to access the slave context :param slave: The slave context to remove """ - raise NotImplementedException() # doesn't make sense here + raise NotImplementedException() # doesn't make sense here def __getitem__(self, slave): """ Used to get access to a slave context diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 73876d78f..8bf14df03 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -1,4 +1,5 @@ import socket +import select import serial import time import sys @@ -230,34 +231,43 @@ def _recv(self, size): """ if not self.socket: raise ConnectionException(self.__str__()) - # 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. + + # 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 + timeout = self.timeout + + # If size isn't specified read 1 byte at a time. + if size is None: + recv_size = 1 else: - while True: - try: - data += self.socket.recv(1) - except socket.error: - pass - if not self.timeout or (time.time() - begin > self.timeout): - break + recv_size = size + + data = b'' + begin = time.time() + while recv_size > 0: + ready = select.select([self.socket], [], [], timeout) + if ready[0]: + data += self.socket.recv(recv_size) + + # If size isn't specified continue to read until timeout expires. + if size: + recv_size = size - len(data) + + # Timeout is reduced also if some data has been received in order + # to avoid infinite loops when there isn't an expected response size + # and the slave sends noisy data continuosly. + timeout -= time.time() - begin + if timeout <= 0: + break + return data def is_socket_open(self): diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index b21d21d42..a99cbc204 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -36,7 +36,7 @@ def __init__(self, *args, **kwargs): self.store['c'] = kwargs.get('co', ModbusSequentialDataBlock.create()) self.store['i'] = kwargs.get('ir', ModbusSequentialDataBlock.create()) self.store['h'] = kwargs.get('hr', ModbusSequentialDataBlock.create()) - self.zero_mode = kwargs.get('zero_mode', Defaults.ZeroMode) + self.zero_mode = kwargs.get('zero_mode', Defaults.ZeroMode) def __str__(self): ''' Returns a string representation of the context @@ -58,19 +58,21 @@ def validate(self, fx, address, count=1): :param count: The number of values to test :returns: True if the request in within range, False otherwise ''' - if not self.zero_mode: address = address + 1 + if not self.zero_mode: + address = address + 1 _logger.debug("validate[%d] %d:%d" % (fx, address, count)) return self.store[self.decode(fx)].validate(address, count) def getValues(self, fx, address, count=1): - ''' Validates the request to make sure it is in range + ''' Get `count` values from datastore :param fx: The function we are working with :param address: The starting address :param count: The number of values to retrieve :returns: The requested values from a:a+c ''' - if not self.zero_mode: address = address + 1 + if not self.zero_mode: + address = address + 1 _logger.debug("getValues[%d] %d:%d" % (fx, address, count)) return self.store[self.decode(fx)].getValues(address, count) @@ -81,7 +83,8 @@ def setValues(self, fx, address, values): :param address: The starting address :param values: The new values to be set ''' - if not self.zero_mode: address = address + 1 + if not self.zero_mode: + address = address + 1 _logger.debug("setValues[%d] %d:%d" % (fx, address, len(values))) self.store[self.decode(fx)].setValues(address, values) diff --git a/pymodbus/datastore/database/redis_datastore.py b/pymodbus/datastore/database/redis_datastore.py index b7c74b013..98bf5bef3 100644 --- a/pymodbus/datastore/database/redis_datastore.py +++ b/pymodbus/datastore/database/redis_datastore.py @@ -5,7 +5,7 @@ #---------------------------------------------------------------------------# # Logging #---------------------------------------------------------------------------# -import logging; +import logging _logger = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def validate(self, fx, address, count=1): return self._val_callbacks[self.decode(fx)](address, count) def getValues(self, fx, address, count=1): - ''' Validates the request to make sure it is in range + ''' Get `count` values from datastore :param fx: The function we are working with :param address: The starting address @@ -94,28 +94,28 @@ def _build_mapping(self): code mapper. ''' self._val_callbacks = { - 'd' : lambda o, c: self._val_bit('d', o, c), - 'c' : lambda o, c: self._val_bit('c', o, c), - 'h' : lambda o, c: self._val_reg('h', o, c), - 'i' : lambda o, c: self._val_reg('i', o, c), + 'd': lambda o, c: self._val_bit('d', o, c), + 'c': lambda o, c: self._val_bit('c', o, c), + 'h': lambda o, c: self._val_reg('h', o, c), + 'i': lambda o, c: self._val_reg('i', o, c), } self._get_callbacks = { - 'd' : lambda o, c: self._get_bit('d', o, c), - 'c' : lambda o, c: self._get_bit('c', o, c), - 'h' : lambda o, c: self._get_reg('h', o, c), - 'i' : lambda o, c: self._get_reg('i', o, c), + 'd': lambda o, c: self._get_bit('d', o, c), + 'c': lambda o, c: self._get_bit('c', o, c), + 'h': lambda o, c: self._get_reg('h', o, c), + 'i': lambda o, c: self._get_reg('i', o, c), } self._set_callbacks = { - 'd' : lambda o, v: self._set_bit('d', o, v), - 'c' : lambda o, v: self._set_bit('c', o, v), - 'h' : lambda o, v: self._set_reg('h', o, v), - 'i' : lambda o, v: self._set_reg('i', o, v), + 'd': lambda o, v: self._set_bit('d', o, v), + 'c': lambda o, v: self._set_bit('c', o, v), + 'h': lambda o, v: self._set_reg('h', o, v), + 'i': lambda o, v: self._set_reg('i', o, v), } #--------------------------------------------------------------------------# # Redis discrete implementation #--------------------------------------------------------------------------# - _bit_size = 16 + _bit_size = 16 _bit_default = '\x00' * (_bit_size % 8) def _get_bit_values(self, key, offset, count): @@ -129,7 +129,7 @@ def _get_bit_values(self, key, offset, count): s = divmod(offset, self._bit_size)[0] e = divmod(offset + count, self._bit_size)[0] - request = ('%s:%s' % (key, v) for v in range(s, e + 1)) + request = ('%s:%s' % (key, v) for v in range(s, e + 1)) response = self.client.mget(request) return response @@ -173,7 +173,7 @@ def _set_bit(self, key, offset, values): current = (r or self._bit_default for r in current) current = ''.join(current) current = current[0:offset] + value.decode('utf-8') + current[offset + count:] - final = (current[s:s + self._bit_size] for s in range(0, count, self._bit_size)) + final = (current[s:s + self._bit_size] for s in range(0, count, self._bit_size)) key = self._get_prefix(key) request = ('%s:%s' % (key, v) for v in range(s, e + 1)) @@ -183,7 +183,7 @@ def _set_bit(self, key, offset, values): #--------------------------------------------------------------------------# # Redis register implementation #--------------------------------------------------------------------------# - _reg_size = 16 + _reg_size = 16 _reg_default = '\x00' * (_reg_size % 8) def _get_reg_values(self, key, offset, count): @@ -198,7 +198,7 @@ def _get_reg_values(self, key, offset, count): #e = divmod(offset+count, self.__reg_size)[0] #request = ('%s:%s' % (key, v) for v in range(s, e + 1)) - request = ('%s:%s' % (key, v) for v in range(offset, count + 1)) + request = ('%s:%s' % (key, v) for v in range(offset, count + 1)) response = self.client.mget(request) return response diff --git a/pymodbus/datastore/database/sql_datastore.py b/pymodbus/datastore/database/sql_datastore.py index a02894251..a0325c2b2 100644 --- a/pymodbus/datastore/database/sql_datastore.py +++ b/pymodbus/datastore/database/sql_datastore.py @@ -10,7 +10,7 @@ #---------------------------------------------------------------------------# # Logging #---------------------------------------------------------------------------# -import logging; +import logging _logger = logging.getLogger(__name__) @@ -57,7 +57,7 @@ def validate(self, fx, address, count=1): return self._validate(self.decode(fx), address, count) def getValues(self, fx, address, count=1): - ''' Validates the request to make sure it is in range + ''' Get `count` values from datastore :param fx: The function we are working with :param address: The starting address @@ -91,10 +91,10 @@ def _db_create(self, table, database): self._engine = sqlalchemy.create_engine(database, echo=False) self._metadata = sqlalchemy.MetaData(self._engine) self._table = sqlalchemy.Table(table, self._metadata, - sqlalchemy.Column('type', sqltypes.String(1)), - sqlalchemy.Column('index', sqltypes.Integer), - sqlalchemy.Column('value', sqltypes.Integer), - UniqueConstraint('type', 'index', name='key')) + sqlalchemy.Column('type', sqltypes.String(1)), + sqlalchemy.Column('index', sqltypes.Integer), + sqlalchemy.Column('value', sqltypes.Integer), + UniqueConstraint('type', 'index', name='key')) self._table.create(checkfirst=True) self._connection = self._engine.connect() @@ -105,7 +105,7 @@ def _get(self, type, offset, count): :param count: The number of bits to read :returns: The resulting values ''' - query = self._table.select(and_( + query = self._table.select(and_( self._table.c.type == type, self._table.c.index >= offset, self._table.c.index <= offset + count) @@ -125,9 +125,9 @@ def _build_set(self, type, offset, values, prefix=''): result = [] for index, value in enumerate(values): result.append({ - prefix + 'type' : type, - prefix + 'index' : offset + index, - 'value' : value + prefix + 'type': type, + prefix + 'index': offset + index, + 'value': value }) return result @@ -144,8 +144,8 @@ def _set(self, type, offset, values): ''' if self._check(type, offset, values): context = self._build_set(type, offset, values) - query = self._table.insert() - result = self._connection.execute(query, context) + query = self._table.insert() + result = self._connection.execute(query, context) return result.rowcount == len(values) else: return False @@ -158,11 +158,11 @@ def _update(self, type, offset, values): :param values: The values to set ''' context = self._build_set(type, offset, values, prefix='x_') - query = self._table.update().values(name='value') - query = query.where(and_( - self._table.c.type == bindparam('x_type'), + query = self._table.update().values(name='value') + query = query.where(and_( + self._table.c.type == bindparam('x_type'), self._table.c.index == bindparam('x_index'))) - result = self._connection.execute(query, context) + result = self._connection.execute(query, context) return result.rowcount == len(values) def _validate(self, type, offset, count): @@ -172,7 +172,7 @@ def _validate(self, type, offset, count): :param count: The number of bits to read :returns: The result of the validation ''' - query = self._table.select(and_( + query = self._table.select(and_( self._table.c.type == type, self._table.c.index >= offset, self._table.c.index <= offset + count)) diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index 3f6726bd8..17161cc7e 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -44,7 +44,7 @@ def validate(self, fx, address, count=1): return not result.isError() def getValues(self, fx, address, count=1): - ''' Validates the request to make sure it is in range + ''' Get `count` values from datastore :param fx: The function we are working with :param address: The starting address @@ -100,6 +100,9 @@ def __extract_result(self, fx, result): a response. TODO make this consistent (values?) ''' if not result.isError(): - if fx in ['d', 'c']: return result.bits - if fx in ['h', 'i']: return result.registers - else: return result + if fx in ['d', 'c']: + return result.bits + if fx in ['h', 'i']: + return result.registers + else: + return result diff --git a/pymodbus/interfaces.py b/pymodbus/interfaces.py index 8587e94e2..4d5843018 100644 --- a/pymodbus/interfaces.py +++ b/pymodbus/interfaces.py @@ -190,7 +190,7 @@ def validate(self, fx, address, count=1): raise NotImplementedException("validate context values") def getValues(self, fx, address, count=1): - ''' Validates the request to make sure it is in range + ''' Get `count` values from datastore :param fx: The function we are working with :param address: The starting address @@ -227,6 +227,7 @@ def build(self): ''' raise NotImplementedException("set context values") + #---------------------------------------------------------------------------# # Exported symbols #---------------------------------------------------------------------------# diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index dff3f10b2..6d38ca3dd 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -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.TRANSACTION_COMPLETE: "TRANSCATION_COMPLETE" + ModbusTransactionState.TRANSACTION_COMPLETE: "TRANSACTION_COMPLETE" } return states.get(state, None) diff --git a/test/test_client_sync.py b/test/test_client_sync.py index 194c46902..5e5c62d7f 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -166,10 +166,12 @@ def testSyncTcpClientInstantiation(self): client = ModbusTcpClient() self.assertNotEqual(client, None) - def testBasicSyncTcpClient(self): + @patch('pymodbus.client.sync.select') + def testBasicSyncTcpClient(self, mock_select): ''' Test the basic methods for the tcp sync client''' # receive/send + mock_select.select.return_value = [True] client = ModbusTcpClient() client.socket = mockSocket() self.assertEqual(0, client._send(None)) @@ -207,8 +209,11 @@ def testTcpClientSend(self): self.assertEqual(0, client._send(None)) self.assertEqual(4, client._send('1234')) - def testTcpClientRecv(self): + @patch('pymodbus.client.sync.select') + def testTcpClientRecv(self, mock_select): ''' Test the tcp client receive method''' + + mock_select.select.return_value = [True] client = ModbusTcpClient() self.assertRaises(ConnectionException, lambda: client._recv(1024)) @@ -223,10 +228,10 @@ def testTcpClientRecv(self): 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') + mock_select.select.return_value = [False] self.assertEqual(b'', client._recv(2)) client.socket = mockSocket() - client.socket.timeout = 0.1 + mock_select.select.return_value = [True] self.assertIn(b'\x00', client._recv(None)) def testSerialClientRpr(self): @@ -351,4 +356,4 @@ def testSerialClientRepr(self): # Main # ---------------------------------------------------------------------------# if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main()