diff --git a/.gitignore b/.gitignore index 3998172c3..3e5d76b47 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ pymodbus.egg-info/ .tox/ doc/api/epydoc/html/ .vscode/ - +.venv __pycache__/ pymodbus/__pycache__/ pymodbus/client/__pycache__/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5ebc9db30..1297df4b8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,4 +1,13 @@ +Version 1.3.2 +------------------------------------------------------------ +* ModbusSerialServer could now be stopped when running on a seperate thread. +* Fix issue with server and client where in the frame buffer had values from previous unsuccesful transaction +* Fix response length calculation for ModbusASCII protocol +* Fix response length calculation ReportSlaveIdResponse, DiagnosticStatusResponse +* Fix never ending transaction case when response is recieved without header and CRC +* Fix tests + Version 1.3.1 ------------------------------------------------------------ * Recall socket recv until get a complete response diff --git a/README.rst b/README.rst index afb4e0730..a41f14f65 100644 --- a/README.rst +++ b/README.rst @@ -125,7 +125,6 @@ like your device tested, I accept devices via mail or by IP address. That said, the current work mainly involves polishing the library as I get time doing such tasks as: - * Add CI support * Make PEP-8 compatible and flake8 ready * Fixing bugs/feature requests * Architecture documentation @@ -152,6 +151,10 @@ Use make to perform a range of activities make tox run the tests on all Python versions make clean cleanup all temporary files +------------------------------------------------------------ +Contributing +------------------------------------------------------------ +Just fork the repo and raise your PR against `dev` branch. ------------------------------------------------------------ License Information diff --git a/examples/common/callback-server.py b/examples/common/callback-server.py index 374787b6c..572b13841 100755 --- a/examples/common/callback-server.py +++ b/examples/common/callback-server.py @@ -16,6 +16,7 @@ from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + #---------------------------------------------------------------------------# # import the python libraries we need #---------------------------------------------------------------------------# @@ -31,7 +32,8 @@ #---------------------------------------------------------------------------# # create your custom data block with callbacks -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# + class CallbackDataBlock(ModbusSparseDataBlock): ''' A datablock that stores the new value in memory and passes the operation to a message queue for further @@ -44,7 +46,7 @@ def __init__(self, devices, queue): self.devices = devices self.queue = queue - values = {k:0 for k in devices.iterkeys()} + values = {k:0 for k in devices.keys()} values[0xbeef] = len(values) # the number of devices super(CallbackDataBlock, self).__init__(values) diff --git a/examples/common/device-mapping b/examples/common/device-mapping new file mode 100644 index 000000000..dc7c8b85c --- /dev/null +++ b/examples/common/device-mapping @@ -0,0 +1,2 @@ +0x0001,/dev/ptyp0 +0x0002,/dev/ptyp1 \ No newline at end of file diff --git a/examples/common/modbus-payload-server.py b/examples/common/modbus-payload-server.py index 3c6d1953a..b2b55ebeb 100755 --- a/examples/common/modbus-payload-server.py +++ b/examples/common/modbus-payload-server.py @@ -35,10 +35,10 @@ # build your payload #---------------------------------------------------------------------------# builder = BinaryPayloadBuilder(endian=Endian.Little) -builder.add_string('abcdefgh') -builder.add_32bit_float(22.34) -builder.add_16bit_uint(0x1234) -builder.add_8bit_int(0x12) +# builder.add_string('abcdefgh') +# builder.add_32bit_float(22.34) +# builder.add_16bit_uint(4660) +# builder.add_8bit_int(18) builder.add_bits([0,1,0,1,1,0,1,0]) #---------------------------------------------------------------------------# diff --git a/examples/common/modbus-payload.py b/examples/common/modbus-payload.py index 03dbad354..cd349387a 100755 --- a/examples/common/modbus-payload.py +++ b/examples/common/modbus-payload.py @@ -9,6 +9,7 @@ from pymodbus.payload import BinaryPayloadDecoder from pymodbus.payload import BinaryPayloadBuilder from pymodbus.client.sync import ModbusTcpClient as ModbusClient +from pymodbus.compat import iteritems #---------------------------------------------------------------------------# # configure the client logging @@ -34,6 +35,7 @@ # - a 8 byte string 'abcdefgh' # - a 32 bit float 22.34 # - a 16 bit unsigned int 0x1234 +# - another 16 bit unsigned int 0x5678 # - an 8 bit int 0x12 # - an 8 bit bitstring [0,1,0,1,1,0,1,0] #---------------------------------------------------------------------------# @@ -41,6 +43,7 @@ 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]) payload = builder.build() @@ -57,6 +60,7 @@ # - a 8 byte string 'abcdefgh' # - a 32 bit float 22.34 # - a 16 bit unsigned int 0x1234 +# - another 16 bit unsigned int which we will ignore # - an 8 bit int 0x12 # - an 8 bit bitstring [0,1,0,1,1,0,1,0] #---------------------------------------------------------------------------# @@ -68,15 +72,16 @@ '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(), } -print "-" * 60 -print "Decoded Data" -print "-" * 60 -for name, value in decoded.iteritems(): - print ("%s\t" % name), value +print("-" * 60) +print("Decoded Data") +print("-" * 60) +for name, value in iteritems(decoded): + print ("%s\t" % name, value) #---------------------------------------------------------------------------# # close the client diff --git a/examples/common/performance.py b/examples/common/performance.py index 36ce11e7d..0a193d9d3 100755 --- a/examples/common/performance.py +++ b/examples/common/performance.py @@ -13,7 +13,7 @@ import logging, os from time import time from multiprocessing import log_to_stderr -from pymodbus.client.sync import ModbusTcpClient +# from pymodbus.client.sync import ModbusTcpClient from pymodbus.client.sync import ModbusSerialClient #---------------------------------------------------------------------------# @@ -61,9 +61,10 @@ def single_client_test(host, cycles): client = ModbusSerialClient(method="rtu", port="/dev/ttyp0", baudrate=9600) while count < cycles: with _thread_lock: - result = client.read_holding_registers(10, 1, unit=1).getRegister(0) + client.read_holding_registers(10, 1, unit=1).registers[0] count += 1 - except: logger.exception("failed to run test successfully") + except: + logger.exception("failed to run test successfully") logger.debug("finished worker: %d" % os.getpid()) #---------------------------------------------------------------------------# @@ -73,6 +74,10 @@ def single_client_test(host, cycles): # threads that was specified. We then start all the threads and block on # them to finish. This may need to switch to another mechanism to signal # finished as the process/thread start up/shut down may skew the test a bit. + +# RTU 32 requests/second @9600 +# TCP 31430 requests/second + #---------------------------------------------------------------------------# args = (host, int(cycles * 1.0 / workers)) procs = [Worker(target=single_client_test, args=args) for _ in range(workers)] diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 7f8511613..80946b77a 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -308,7 +308,10 @@ def __init__(self, method='ascii', **kwargs): self.timeout = kwargs.get('timeout', Defaults.Timeout) if self.method == "rtu": self._last_frame_end = 0.0 - self._silent_interval = 3.5 * (1 + 8 + 2) / self.baudrate + if self.baudrate > 19200: + self._silent_interval = 1.75/1000 # ms + else: + self._silent_interval = 3.5 * (1 + 8 + 2) / self.baudrate @staticmethod def __implementation(method): diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index ead3d36cc..f6c02cb1c 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -74,7 +74,6 @@ def get_response_pdu_size(self): return 1 + 2 + 2 * len(self.message) - class DiagnosticStatusResponse(ModbusResponse): ''' This is a base class for all of the diagnostic response functions @@ -118,7 +117,11 @@ def decode(self, data): :param data: The data to decode into the function code ''' - self.sub_function_code, self.message = struct.unpack('>HH', data) + word_len = len(data)//2 + if len(data) % 2: + word_len += 1 + data = struct.unpack('>' + 'H'*word_len, data) + self.sub_function_code, self.message = data[0], data[1:] class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest): @@ -708,6 +711,23 @@ class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest): ''' sub_function_code = 0x0015 + def __init__(self, **kwargs): + super(GetClearModbusPlusRequest, self).__init__(**kwargs) + + def get_response_pdu_size(self): + """ + Returns a series of 54 16-bit words (108 bytes) in the data field of the response + (this function differs from the usual two-byte length of the data field). The data + contains the statistics for the Modbus Plus peer processor in the slave device. + Func_code (1 byte) + Sub function code (2 byte) + Operation (2 byte) + Data (108 bytes) + :return: + """ + if self.message == ModbusPlusOperation.GetStatistics: + data = 2 + 108 # byte count(2) + data (54*2) + else: + data = 0 + return 1 + 2 + 2 + 2+ data + def execute(self, *args): ''' Execute the diagnostic request on the given device @@ -716,9 +736,23 @@ def execute(self, *args): message = None # the clear operation does not return info if self.message == ModbusPlusOperation.ClearStatistics: _MCB.Plus.reset() - else: message = _MCB.Plus.encode() + message = self.message + else: + message = [self.message] + message += _MCB.Plus.encode() return GetClearModbusPlusResponse(message) + def encode(self): + ''' + Base encoder for a diagnostic response + we encode the data set in self.message + + :returns: The encoded packet + ''' + packet = struct.pack('>H', self.sub_function_code) + packet += struct.pack('>H', self.message) + return packet + class GetClearModbusPlusResponse(DiagnosticStatusSimpleResponse): ''' diff --git a/pymodbus/exceptions.py b/pymodbus/exceptions.py index c143569f5..a2ad48241 100644 --- a/pymodbus/exceptions.py +++ b/pymodbus/exceptions.py @@ -78,19 +78,19 @@ def __init__(self, string=""): ModbusException.__init__(self, message) -class InvalidResponseRecievedException(ModbusException): +class InvalidMessageRecievedException(ModbusException): """ Error resulting from invalid response received or decoded """ + def __init__(self, string=""): + ''' Initialize the exception -def __init__(self, string=""): - ''' Initialize the exception + :param string: The message to append to the error + ''' + message = "[Invalid Message] %s" % string + ModbusException.__init__(self, message) - :param string: The message to append to the error - ''' - message = "[Invalid Response] %s" % string - ModbusException.__init__(self, message) #---------------------------------------------------------------------------# # Exported symbols diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index 93c6cfb55..b30ea7598 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -403,7 +403,7 @@ def encode(self): status = ModbusStatus.SlaveOn else: status = ModbusStatus.SlaveOff - length = len(self.identifier) + 2 + length = len(self.identifier) + 1 packet = int2byte(length) packet += self.identifier # we assume it is already encoded packet += int2byte(status) diff --git a/pymodbus/payload.py b/pymodbus/payload.py index a34a10fe0..cbc722136 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -349,6 +349,14 @@ def decode_string(self, size=1): self._pointer += size return self._payload[self._pointer - size:self._pointer] + def skip_bytes(self, nbytes): + ''' Skip n bytes in the buffer + + :param nbytes: The number of bytes to skip + ''' + self._pointer += nbytes + return None + #---------------------------------------------------------------------------# # Exported Identifiers #---------------------------------------------------------------------------# diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 55d3180e9..4559513f2 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -28,6 +28,7 @@ #---------------------------------------------------------------------------# # Protocol Handlers #---------------------------------------------------------------------------# + class ModbusBaseRequestHandler(socketserver.BaseRequestHandler): ''' Implements the modbus server protocol @@ -85,34 +86,6 @@ def send(self, message): raise NotImplementedException("Method not implemented by derived class") -# class ModbusSingleRequestHandler(ModbusBaseRequestHandler): -# ''' 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 -# ''' -# while self.running: -# try: -# data = self.request.recv(1024) -# if data: -# if _logger.isEnabledFor(logging.DEBUG): -# _logger.debug(' '.join([hex(byte2int(x)) for x in data])) -# self.framer.processIncomingPacket(data, self.execute) -# except Exception as msg: -# # since we only have a single socket, we cannot exit -# _logger.error("Socket error occurred %s" % msg) -# -# def send(self, message): -# ''' Send a request (string) to the network -# -# :param message: The unencoded modbus response -# ''' - - class ModbusSingleRequestHandler(ModbusBaseRequestHandler): ''' Implements the modbus server protocol @@ -132,6 +105,8 @@ def handle(self): self.framer.processIncomingPacket(data, self.execute) except Exception as msg: # since we only have a single socket, we cannot exit + # Clear frame buffer + self.framer.resetFrame() _logger.error("Socket error occurred %s" % msg) def send(self, message): @@ -147,6 +122,16 @@ def send(self, message): return self.request.send(pdu) +class CustomSingleRequestHandler(ModbusSingleRequestHandler): + + def __init__(self, request, client_address, server): + self.request = request + self.client_address = client_address + self.server = server + self.running = True + self.setup() + + class ModbusConnectedRequestHandler(ModbusBaseRequestHandler): ''' Implements the modbus server protocol @@ -167,6 +152,7 @@ def handle(self): to supply the alternative request handler class. ''' + reset_frame = False while self.running: try: data = self.request.recv(1024) @@ -178,13 +164,18 @@ def handle(self): except socket.timeout as msg: if _logger.isEnabledFor(logging.DEBUG): _logger.debug("Socket timeout occurred %s", msg) - pass + reset_frame = True except socket.error as msg: _logger.error("Socket error occurred %s" % msg) self.running = False except: _logger.error("Socket exception occurred %s" % traceback.format_exc() ) self.running = False + reset_frame = True + finally: + if reset_frame: + self.framer.resetFrame() + reset_frame = False def send(self, message): ''' Send a request (string) to the network @@ -211,10 +202,12 @@ class ModbusDisconnectedRequestHandler(ModbusBaseRequestHandler): def handle(self): ''' Callback when we receive any data ''' + reset_frame = False while self.running: try: data, self.request = self.request - if not data: self.running = False + if not data: + self.running = False if _logger.isEnabledFor(logging.DEBUG): _logger.debug(' '.join([hex(byte2int(x)) for x in data])) # if not self.server.control.ListenOnly: @@ -223,7 +216,15 @@ def handle(self): except socket.error as msg: _logger.error("Socket error occurred %s" % msg) self.running = False - except: self.running = False + reset_frame = True + except Exception as msg: + _logger.error(msg) + self.running = False + reset_frame = True + finally: + if reset_frame: + self.framer.resetFrame() + reset_frame = False def send(self, message): ''' Send a request (string) to the network @@ -368,6 +369,8 @@ class ModbusSerialServer(object): server context instance. ''' + handler = None + def __init__(self, context, framer=None, identity=None, **kwargs): ''' Overloaded initializer for the socket server @@ -402,8 +405,9 @@ def __init__(self, context, framer=None, identity=None, **kwargs): self.timeout = kwargs.get('timeout', Defaults.Timeout) self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) self.socket = None - self._connect() - self.is_running = True + if self._connect(): + self.is_running = True + self._build_handler() def _connect(self): ''' Connect to the serial server @@ -425,12 +429,13 @@ def _build_handler(self): :returns: A patched handler ''' + request = self.socket request.send = request.write request.recv = request.read - handler = ModbusSingleRequestHandler(request, - (self.device, self.device), self) - return handler + self.handler = CustomSingleRequestHandler(request, + (self.device, self.device), + self) def serve_forever(self): ''' Callback for connecting a new client thread @@ -438,16 +443,23 @@ def serve_forever(self): :param request: The request to handle :param client: The address of the client ''' - _logger.debug("Started thread to serve client") - handler = self._build_handler() - while self.is_running: - handler.handle() + if self._connect(): + _logger.debug("Started thread to serve client") + if not self.handler: + self._build_handler() + while self.is_running: + self.handler.handle() + else: + _logger.error("Error opening serial port , Unable to start server!!") def server_close(self): ''' Callback for stopping the running server ''' _logger.debug("Modbus server stopped") self.is_running = False + self.handler.finish() + self.handler.running = False + self.handler = None self.socket.close() diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 9b546975b..a70c268a4 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -7,7 +7,7 @@ from binascii import b2a_hex, a2b_hex from pymodbus.exceptions import ModbusIOException, NotImplementedException -from pymodbus.exceptions import InvalidResponseRecievedException +from pymodbus.exceptions import InvalidMessageRecievedException from pymodbus.constants import Defaults from pymodbus.interfaces import IModbusFramer from pymodbus.utilities import checkCRC, computeCRC @@ -61,7 +61,7 @@ def _set_adu_size(self): elif isinstance(self.client.framer, ModbusRtuFramer): self.base_adu_size = 3 # address(1), CRC(2) elif isinstance(self.client.framer, ModbusAsciiFramer): - self.base_adu_size = 4 # Address(2), LRC(2) + self.base_adu_size = 7 # start(1)+ Address(2), LRC(2) + end(2) elif isinstance(self.client.framer, ModbusBinaryFramer): self.base_adu_size = 3 #, Address(1), CRC(2) else: @@ -95,7 +95,8 @@ def _check_response(self, response): elif isinstance(self.client.framer, ModbusAsciiFramer): if len(response) >= 5 and int(response[3:5], 16) > 128: return False - elif isinstance(self.client.framer, (ModbusRtuFramer, ModbusBinaryFramer)): + elif isinstance(self.client.framer, (ModbusRtuFramer, + ModbusBinaryFramer)): if len(response) >= 2 and byte2int(response[1]) > 128: return False @@ -108,9 +109,12 @@ def execute(self, 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 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) @@ -121,15 +125,9 @@ def execute(self, request): packet = self.client.framer.buildPacket(request) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("send: " + " ".join([hex(byte2int(x)) for x in packet])) - self.client._send(packet) - exception = False - 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 - result += self.client._recv(expected_response_length - len(result)) + self._send(packet) + # exception = False + result = self._recv(expected_response_length or 1024) if not result and self.retry_on_empty: retries -= 1 @@ -139,19 +137,49 @@ def execute(self, request): _logger.debug("recv: " + " ".join([hex(byte2int(x)) for x in result])) self.client.framer.processIncomingPacket(result, self.addTransaction) break - except (socket.error, ModbusIOException, InvalidResponseRecievedException) as msg: + 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: - last_exception = last_exception or ("No Response " - "received from the remote unit") - response = ModbusIOException(last_exception) + 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) return response + 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 + if isinstance(self.client.framer, ModbusSocketFramer): + break + r = self.client._recv(expected_response_length - len(result)) + if not r: + # If no response being recived there is no point in conitnuing + break + result += r + if result: + break + retries -= 1 + return result + + def addTransaction(self, request, tid=None): ''' Adds a transaction to the handler @@ -439,7 +467,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 InvalidResponseRecievedException(result) + raise InvalidMessageRecievedException(result) else: self.populateResult(result) self.advanceFrame() @@ -453,7 +481,7 @@ def resetFrame(self): end of the message (python just doesn't have the resolution to check for millisecond delays). ''' - self.__buffer = '' + self.__buffer = b'' self.__header = {} def getRawFrame(self): @@ -680,7 +708,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 InvalidResponseRecievedException(result) + raise InvalidMessageRecievedException(result) else: self.populateResult(result) self.advanceFrame() @@ -786,6 +814,17 @@ def getFrame(self): 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 @@ -1018,6 +1057,17 @@ def _preflight(self, data): 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 #---------------------------------------------------------------------------# diff --git a/pymodbus/version.py b/pymodbus/version.py index 2fc9d29d9..f95e10aa0 100644 --- a/pymodbus/version.py +++ b/pymodbus/version.py @@ -41,7 +41,8 @@ def __str__(self): return '[%s, version %s]' % (self.package, self.short()) -version = Version('pymodbus', 1, 3, 1) +version = Version('pymodbus', 1, 3, 2) + version.__name__ = 'pymodbus' # fix epydoc error diff --git a/test/test_client_sync.py b/test/test_client_sync.py index c88d68233..516b44298 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -202,6 +202,13 @@ def testSyncSerialClientInstantiation(self): self.assertTrue(isinstance(ModbusSerialClient(method='binary').framer, ModbusBinaryFramer)) self.assertRaises(ParameterException, lambda: ModbusSerialClient(method='something')) + def testSyncSerialRTUClientTimeouts(self): + client = ModbusSerialClient(method="rtu", baudrate=9600) + assert client._silent_interval == (3.5 * 11/9600) + client = ModbusSerialClient(method="rtu", baudrate=38400) + assert client._silent_interval == (1.75/1000) + + @patch("serial.Serial") def testBasicSyncSerialClient(self, mock_serial): ''' Test the basic methods for the serial sync client''' diff --git a/test/test_diag_messages.py b/test/test_diag_messages.py index e62b8ec5b..ecb093951 100644 --- a/test/test_diag_messages.py +++ b/test/test_diag_messages.py @@ -33,7 +33,7 @@ def setUp(self): (ReturnSlaveBusCharacterOverrunCountRequest, b'\x00\x12\x00\x00', b'\x00\x12\x00\x00'), (ReturnIopOverrunCountRequest, b'\x00\x13\x00\x00', b'\x00\x13\x00\x00'), (ClearOverrunCountRequest, b'\x00\x14\x00\x00', b'\x00\x14\x00\x00'), - (GetClearModbusPlusRequest, b'\x00\x15\x00\x00', b'\x00\x15' + b'\x00\x00' * 55), + (GetClearModbusPlusRequest, b'\x00\x15\x00\x00', b'\x00\x15\x00\x00' + b'\x00\x00' * 55), ] self.responses = [ @@ -56,7 +56,7 @@ def setUp(self): (ReturnSlaveBusCharacterOverrunCountResponse, b'\x00\x12\x00\x00'), (ReturnIopOverrunCountResponse, b'\x00\x13\x00\x00'), (ClearOverrunCountResponse, b'\x00\x14\x00\x00'), - (GetClearModbusPlusResponse, b'\x00\x15' + b'\x00\x00' * 55), + (GetClearModbusPlusResponse, b'\x00\x15\x00\x04' + b'\x00\x00' * 55), ] def tearDown(self): @@ -100,7 +100,8 @@ def testDiagnosticRequestsEncode(self): def testDiagnosticExecute(self): ''' Testing diagnostic message execution ''' for message, encoded, executed in self.requests: - self.assertEqual(message().execute().encode(), executed) + encoded = message().execute().encode() + self.assertEqual(encoded, executed) def testReturnQueryDataRequest(self): ''' Testing diagnostic message execution ''' @@ -130,13 +131,14 @@ def testRestartCommunicationsOption(self): def testGetClearModbusPlusRequestExecute(self): ''' Testing diagnostic message execution ''' - request = GetClearModbusPlusRequest(ModbusPlusOperation.ClearStatistics); + request = GetClearModbusPlusRequest(data=ModbusPlusOperation.ClearStatistics); response = request.execute() - self.assertEqual(response.message, None) + self.assertEqual(response.message, ModbusPlusOperation.ClearStatistics) - request = GetClearModbusPlusRequest(ModbusPlusOperation.GetStatistics); + request = GetClearModbusPlusRequest(data=ModbusPlusOperation.GetStatistics); response = request.execute() - self.assertEqual(response.message, [0x00] * 55) + resp = [ModbusPlusOperation.GetStatistics] + self.assertEqual(response.message, resp+[0x00] * 55) #---------------------------------------------------------------------------# # Main diff --git a/test/test_other_messages.py b/test/test_other_messages.py index 1e9437ac5..08b24a2ba 100644 --- a/test/test_other_messages.py +++ b/test/test_other_messages.py @@ -92,13 +92,14 @@ def testReportSlaveId(self): self.assertEqual(request.execute().function_code, 0x11) response = ReportSlaveIdResponse(request.execute().identifier, True) - self.assertEqual(response.encode(), b'\x0aPymodbus\xff') + + self.assertEqual(response.encode(), b'\tPymodbus\xff') response.decode(b'\x03\x12\x00') self.assertEqual(response.status, False) self.assertEqual(response.identifier, b'\x12\x00') response.status = False - self.assertEqual(response.encode(), b'\x04\x12\x00\x00') + self.assertEqual(response.encode(), b'\x03\x12\x00\x00') #---------------------------------------------------------------------------# # Main diff --git a/test/test_payload.py b/test/test_payload.py index 157f5846a..2375c9590 100644 --- a/test/test_payload.py +++ b/test/test_payload.py @@ -31,13 +31,15 @@ def setUp(self): b'\x01\x02\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00' \ b'\x00\x00\x00\xff\xfe\xff\xfd\xff\xff\xff\xfc\xff' \ b'\xff\xff\xff\xff\xff\xff\x00\x00\xa0\x3f\x00\x00' \ - b'\x00\x00\x00\x00\x19\x40\x74\x65\x73\x74\x11' + b'\x00\x00\x00\x00\x19\x40\x01\x00\x74\x65\x73\x74' \ + b'\x11' self.big_endian_payload = \ b'\x01\x00\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00' \ b'\x00\x00\x04\xff\xff\xfe\xff\xff\xff\xfd\xff\xff' \ b'\xff\xff\xff\xff\xff\xfc\x3f\xa0\x00\x00\x40\x19' \ - b'\x00\x00\x00\x00\x00\x00\x74\x65\x73\x74\x11' + b'\x00\x00\x00\x00\x00\x00\x00\x01\x74\x65\x73\x74' \ + b'\x11' self.bitstring = [True, False, False, False, True, False, False, False] @@ -62,6 +64,7 @@ def testLittleEndianPayloadBuilder(self): builder.add_64bit_int(-4) builder.add_32bit_float(1.25) builder.add_64bit_float(6.25) + builder.add_16bit_uint(1) # placeholder builder.add_string(b'test') builder.add_bits(self.bitstring) self.assertEqual(self.little_endian_payload, builder.to_string()) @@ -79,6 +82,7 @@ def testBigEndianPayloadBuilder(self): builder.add_64bit_int(-4) builder.add_32bit_float(1.25) builder.add_64bit_float(6.25) + builder.add_16bit_uint(1) # placeholder builder.add_string('test') builder.add_bits(self.bitstring) self.assertEqual(self.big_endian_payload, builder.to_string()) @@ -125,6 +129,7 @@ def testLittleEndianPayloadDecoder(self): self.assertEqual(-4, decoder.decode_64bit_int()) self.assertEqual(1.25, decoder.decode_32bit_float()) self.assertEqual(6.25, decoder.decode_64bit_float()) + self.assertEqual(None, decoder.skip_bytes(2)) self.assertEqual('test', decoder.decode_string(4).decode()) self.assertEqual(self.bitstring, decoder.decode_bits()) @@ -141,6 +146,7 @@ def testBigEndianPayloadDecoder(self): self.assertEqual(-4, decoder.decode_64bit_int()) self.assertEqual(1.25, decoder.decode_32bit_float()) self.assertEqual(6.25, decoder.decode_64bit_float()) + self.assertEqual(None, decoder.skip_bytes(2)) self.assertEqual(b'test', decoder.decode_string(4)) self.assertEqual(self.bitstring, decoder.decode_bits()) diff --git a/test/test_server_sync.py b/test/test_server_sync.py index e6a974358..348f2ff4c 100644 --- a/test/test_server_sync.py +++ b/test/test_server_sync.py @@ -33,6 +33,7 @@ def __init__(self): self.threads = [] self.context = {} + #---------------------------------------------------------------------------# # Fixture #---------------------------------------------------------------------------# @@ -280,24 +281,30 @@ def testUdpServerProcess(self): #-----------------------------------------------------------------------# def testSerialServerConnect(self): with patch.object(serial, 'Serial') as mock_serial: - mock_serial.return_value = "socket" - identity = ModbusDeviceIdentification(info={0x00: 'VendorName'}) - server = ModbusSerialServer(context=None, identity=identity) - self.assertEqual(server.socket, "socket") - self.assertEqual(server.control.Identity.VendorName, 'VendorName') - - server._connect() - self.assertEqual(server.socket, "socket") + # mock_serial.return_value = "socket" + mock_serial.write = lambda x: len(x) + mock_serial.read = lambda size: '\x00' * size + identity = ModbusDeviceIdentification(info={0x00: 'VendorName'}) + server = ModbusSerialServer(context=None, identity=identity, port="dummy") + # # mock_serial.return_value = "socket" + # self.assertEqual(server.socket.port, "dummy") + self.assertEquals(server.handler.__class__.__name__, "CustomSingleRequestHandler") + self.assertEqual(server.control.Identity.VendorName, 'VendorName') + + server._connect() + # self.assertEqual(server.socket, "socket") with patch.object(serial, 'Serial') as mock_serial: + mock_serial.write = lambda x: len(x) + mock_serial.read = lambda size: '\x00' * size mock_serial.side_effect = serial.SerialException() - server = ModbusSerialServer(None) + server = ModbusSerialServer(None, port="dummy") self.assertEqual(server.socket, None) def testSerialServerServeForever(self): ''' test that the synchronous serial server closes correctly ''' with patch.object(serial, 'Serial') as mock_serial: - with patch('pymodbus.server.sync.ModbusSingleRequestHandler') as mock_handler: + with patch('pymodbus.server.sync.CustomSingleRequestHandler') as mock_handler: server = ModbusSerialServer(None) instance = mock_handler.return_value instance.handle.side_effect = server.server_close