diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a2fffd0c9..6a9e87bd0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,24 @@ +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()) +* 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 UDP client issue while handling MEI messages (ReadDeviceInformationRequest) +* Add expected response lengths for WriteMultipleCoilRequest and WriteMultipleRegisterRequest +* Fix _rtu_byte_count_pos for GetCommEventLogResponse +* Add support for repeated MEI device information Object IDs +* 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 ------------------------------------------------------------ * Bug fix Modbus TCP client reading incomplete data 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/README.rst b/README.rst index a41f14f65..9d682b7b5 100644 --- a/README.rst +++ b/README.rst @@ -75,7 +75,7 @@ For those of you that just want to get started fast, here you go:: client = ModbusTcpClient('127.0.0.1') client.write_coil(1, True) result = client.read_coils(1,1) - print result.bits[0] + print(result.bits[0]) client.close() For more advanced examples, check out the examples included in the 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..5ac59c3ae 100755 --- a/examples/common/asynchronous_client.py +++ b/examples/common/asynchronous_client.py @@ -6,27 +6,30 @@ The following is an example of how to use the asynchronous modbus client implementation from pymodbus. """ -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # import needed libraries -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # from twisted.internet import reactor, protocol from pymodbus.constants import Defaults -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # 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) -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # helper method to test deferred callbacks # --------------------------------------------------------------------------- # @@ -34,25 +37,36 @@ def dassert(deferred, callback): def _assertor(value): assert value - deferred.addCallback(lambda r: _assertor(callback(r))) deferred.addErrback(lambda _: _assertor(False)) -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # specify slave to query -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # The slave to query is specified in an optional parameter for each # individual request. This can be done by specifying the `unit` parameter # which defaults to `0x00` # --------------------------------------------------------------------------- # +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 -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # simply call the methods that you would like to use. An example session # is displayed below along with some assert checks. Note that unlike the # synchronous version of the client, the asynchronous version returns @@ -61,54 +75,59 @@ 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) rr = client.read_coils(1, 1, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error + dassert(rq, lambda r: not r.isError()) # test for no error dassert(rr, lambda r: r.bits[0] == True) # test the expected value - + rq = client.write_coils(1, [True]*8, unit=UNIT) rr = client.read_coils(1, 8, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error + dassert(rq, lambda r: not r.isError()) # test for no error dassert(rr, lambda r: r.bits == [True]*8) # test the expected value - + rq = client.write_coils(1, [False]*8, unit=UNIT) rr = client.read_discrete_inputs(1, 8, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error + dassert(rq, lambda r: not r.isError()) # test for no error dassert(rr, lambda r: r.bits == [True]*8) # test the expected value - + rq = client.write_register(1, 10, unit=UNIT) rr = client.read_holding_registers(1, 1, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error + dassert(rq, lambda r: not r.isError()) # test for no error dassert(rr, lambda r: r.registers[0] == 10) # test the expected value - + rq = client.write_registers(1, [10]*8, unit=UNIT) rr = client.read_input_registers(1, 8, unit=UNIT) - dassert(rq, lambda r: r.function_code < 0x80) # test for no error + dassert(rq, lambda r: not r.isError()) # test for no error dassert(rr, lambda r: r.registers == [17]*8) # test the expected value - + arguments = { 'read_address': 1, 'read_count': 8, 'write_address': 1, 'write_registers': [20]*8, } - rq = client.readwrite_registers(**arguments, unit=UNIT) + rq = client.readwrite_registers(arguments, unit=UNIT) 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 -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # If you are performing a request that is not available in the client # mixin, you have to perform the request like this instead:: # @@ -120,11 +139,11 @@ def beginAsynchronousTest(client): # if isinstance(response, ClearCountersResponse): # ... do something with the response # -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # choose the client you want -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # make sure to start an implementation to hit against. For this # you can use an existing device, the reference implementation in the tools # directory, or start a pymodbus server. @@ -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/changing_framers.py b/examples/common/changing_framers.py index 4391d7a00..7a0c1b571 100755 --- a/examples/common/changing_framers.py +++ b/examples/common/changing_framers.py @@ -14,23 +14,23 @@ tcp transport with an RTU framer). However, please let us know of any success cases that are not documented! """ -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # import the modbus client and the framers -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # from pymodbus.client.sync import ModbusTcpClient as ModbusClient -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # Import the modbus framer that you want -# --------------------------------------------------------------------------- # -# --------------------------------------------------------------------------- # -#from pymodbus.transaction import ModbusSocketFramer as ModbusFramer -from pymodbus.transaction import ModbusRtuFramer as ModbusFramer +# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # +from pymodbus.transaction import ModbusSocketFramer as ModbusFramer +# from pymodbus.transaction import ModbusRtuFramer as ModbusFramer #from pymodbus.transaction import ModbusBinaryFramer as ModbusFramer #from pymodbus.transaction import ModbusAsciiFramer as ModbusFramer -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # configure the client logging -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # import logging logging.basicConfig() log = logging.getLogger() @@ -48,7 +48,7 @@ # ----------------------------------------------------------------------- # rq = client.write_coil(1, True) rr = client.read_coils(1,1) - assert(rq.function_code < 0x80) # test that we are not an error + assert(not rq.isError()) # test that we are not an error assert(rr.bits[0] == True) # test the expected value # ----------------------------------------------------------------------- # diff --git a/examples/common/modbus_payload.py b/examples/common/modbus_payload.py index a9d74cd0e..8bcb374ff 100755 --- a/examples/common/modbus_payload.py +++ b/examples/common/modbus_payload.py @@ -10,12 +10,16 @@ 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) @@ -24,7 +28,7 @@ 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..9afc9d1c2 100755 --- a/examples/common/synchronous_client.py +++ b/examples/common/synchronous_client.py @@ -13,18 +13,20 @@ result = client.read_coils(1,10) print result """ -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # 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 -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # 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) @@ -32,9 +34,9 @@ def run_sync_client(): - # ------------------------------------------------------------------------# + # ------------------------------------------------------------------------# # choose the client you want - # ------------------------------------------------------------------------# + # ------------------------------------------------------------------------# # make sure to start an implementation to hit against. For this # you can use an existing device, the reference implementation in the tools # directory, or start a pymodbus server. @@ -58,23 +60,28 @@ def run_sync_client(): # Here is an example of using these options:: # # 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) + # 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) + # client = ModbusClient(method='rtu', port='/dev/ptyp0', timeout=1, + # baudrate=9600) client.connect() - - # ------------------------------------------------------------------------# + + # ------------------------------------------------------------------------# # specify slave to query - # ------------------------------------------------------------------------# + # ------------------------------------------------------------------------# # The slave to query is specified in an optional parameter for each # individual request. This can be done by specifying the `unit` parameter # 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 # ----------------------------------------------------------------------- # @@ -90,49 +97,49 @@ def run_sync_client(): log.debug("Write to a Coil and read back") rq = client.write_coil(0, True, unit=UNIT) rr = client.read_coils(0, 1, unit=UNIT) - assert(rq.function_code < 0x80) # test that we are not an error + assert(not rq.isError()) # test that we are not an error assert(rr.bits[0] == True) # test the expected value - + log.debug("Write to multiple coils and read back- test 1") rq = client.write_coils(1, [True]*8, unit=UNIT) - assert(rq.function_code < 0x80) # test that we are not an error + assert(not rq.isError()) # test that we are not an error rr = client.read_coils(1, 21, unit=UNIT) - assert(rr.function_code < 0x80) # test that we are not an error + assert(not rr.isError()) # test that we are not an error resp = [True]*21 - + # If the returned output quantity is not a multiple of eight, # the remaining bits in the final data byte will be padded with zeros # (toward the high order end of the byte). - + resp.extend([False]*3) assert(rr.bits == resp) # test the expected value - + log.debug("Write to multiple coils and read back - test 2") rq = client.write_coils(1, [False]*8, unit=UNIT) rr = client.read_coils(1, 8, unit=UNIT) - assert(rq.function_code < 0x80) # test that we are not an error + assert(not rq.isError()) # test that we are not an error assert(rr.bits == [False]*8) # test the expected value - + log.debug("Read discrete inputs") rr = client.read_discrete_inputs(0, 8, unit=UNIT) - assert(rq.function_code < 0x80) # test that we are not an error - + assert(not rq.isError()) # test that we are not an error + log.debug("Write to a holding register and read back") rq = client.write_register(1, 10, unit=UNIT) rr = client.read_holding_registers(1, 1, unit=UNIT) - assert(rq.function_code < 0x80) # test that we are not an error + assert(not rq.isError()) # test that we are not an error assert(rr.registers[0] == 10) # test the expected value - + log.debug("Write to multiple holding registers and read back") rq = client.write_registers(1, [10]*8, unit=UNIT) rr = client.read_holding_registers(1, 8, unit=UNIT) - assert(rq.function_code < 0x80) # test that we are not an error + assert(not rq.isError()) # test that we are not an error assert(rr.registers == [10]*8) # test the expected value - + log.debug("Read input registers") rr = client.read_input_registers(1, 8, unit=UNIT) - assert(rq.function_code < 0x80) # test that we are not an error - + assert(not rq.isError()) # test that we are not an error + arguments = { 'read_address': 1, 'read_count': 8, @@ -142,10 +149,10 @@ def run_sync_client(): log.debug("Read write registeres simulataneously") rq = client.readwrite_registers(unit=UNIT, **arguments) rr = client.read_holding_registers(1, 8, unit=UNIT) - assert(rq.function_code < 0x80) # test that we are not an error + assert(not rq.isError()) # test that we are not an error assert(rq.registers == [20]*8) # test the expected value assert(rr.registers == [20]*8) # test the expected value - + # ----------------------------------------------------------------------- # # close the client # ----------------------------------------------------------------------- # diff --git a/examples/common/synchronous_client_ext.py b/examples/common/synchronous_client_ext.py index 637873a6f..72f7309a8 100755 --- a/examples/common/synchronous_client_ext.py +++ b/examples/common/synchronous_client_ext.py @@ -7,26 +7,29 @@ implementation from pymodbus to perform the extended portions of the modbus protocol. """ -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # 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 ModbusSerialClient as ModbusClient + # --------------------------------------------------------------------------- # # import the extended messages to perform -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # from pymodbus.diag_message import * from pymodbus.file_message import * from pymodbus.other_message import * from pymodbus.mei_message import * -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # 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,23 +37,27 @@ def execute_extended_requests(): - # ------------------------------------------------------------------------# + # ------------------------------------------------------------------------# # choose the client you want - # ------------------------------------------------------------------------# + # ------------------------------------------------------------------------# # make sure to start an implementation to hit against. For this # you can use an existing device, the reference implementation in the tools # directory, or start a pymodbus server. # - # It should be noted that you can supply an ipv4 or an ipv6 host address + # 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) + # from pymodbus.transaction import ModbusRtuFramer + # client = ModbusClient('127.0.0.1', port=5020, framer=ModbusRtuFramer) client.connect() - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # # extra requests - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # # If you are performing a request that is not available in the client # mixin, you have to perform the request like this instead:: # @@ -65,17 +72,17 @@ def execute_extended_requests(): # # What follows is a listing of all the supported methods. Feel free to # comment, uncomment, or modify each result set to match with your ref. - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # - # ----------------------------------------------------------------------- # + # ----------------------------------------------------------------------- # # information 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 (not rr.isError()) # test that we are not an error # assert (rr.information[0] == b'Pymodbus') # test the vendor name # assert (rr.information[1] == b'PM') # test the product code # assert (rr.information[2] == b'1.0') # test the code revision @@ -83,143 +90,143 @@ 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(not rr.isError()) # test that we are not an error # assert(rr.identifier == 0x00) # test the slave identifier # assert(rr.status == 0x00) # test that the status is ok 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(not rr.isError()) # test that we are not an error # assert(rr.status == 0x55) # test the status code 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(not rr.isError()) # test that we are not an error # assert(rr.status == True) # test the status code # assert(rr.count == 0x00) # test the status code 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(not rr.isError()) # test that we are not an error # assert(rr.status == True) # test the status code # assert(rr.event_count == 0x00) # test the number of events # assert(rr.message_count == 0x00) # test the number of messages # assert(len(rr.events) == 0x00) # test the number of events - # ------------------------------------------------------------------------# + # ------------------------------------------------------------------------# # diagnostic 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 - # ------------------------------------------------------------------------# + # ------------------------------------------------------------------------# # close the client - # ------------------------------------------------------------------------# + # ------------------------------------------------------------------------# client.close() diff --git a/examples/common/synchronous_server.py b/examples/common/synchronous_server.py index e5ad9486e..617b1acc9 100755 --- a/examples/common/synchronous_server.py +++ b/examples/common/synchronous_server.py @@ -16,15 +16,17 @@ 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 +from pymodbus.transaction import ModbusRtuFramer, 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) @@ -85,10 +87,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)) + 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) # ----------------------------------------------------------------------- # @@ -102,24 +105,35 @@ 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", 5020)) - + + # TCP with different framer + # StartTcpServer(context, identity=identity, + # framer=ModbusRtuFramer, address=("0.0.0.0", 5020)) + # Udp: - # StartUdpServer(context, identity=identity, address=("localhost", 502)) + # StartUdpServer(context, identity=identity, address=("0.0.0.0", 5020)) # Ascii: - # StartSerialServer(context, identity=identity, - # port='/dev/pts/3', timeout=1) + # StartSerialServer(context, identity=identity, + # port='/dev/ttyp0', timeout=1) # RTU: # StartSerialServer(context, framer=ModbusRtuFramer, identity=identity, - # port='/dev/ptyp0', 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/concurrent_client.py b/examples/contrib/concurrent_client.py index b3873f845..068331cc0 100755 --- a/examples/contrib/concurrent_client.py +++ b/examples/contrib/concurrent_client.py @@ -214,7 +214,10 @@ def execute(self, request): :param request: The request to execute :returns: A future linked to the call's response """ - fut, work_id = Future(), self.counter.next() + if IS_PYTHON3: + fut, work_id = Future(), next(self.counter) + else: + fut, work_id = Future(), self.counter.next() self.input_queue.put(WorkRequest(request, work_id)) self.futures[work_id] = fut return fut diff --git a/examples/contrib/message_parser.py b/examples/contrib/message_parser.py index bd440fd55..73d109931 100755 --- a/examples/contrib/message_parser.py +++ b/examples/contrib/message_parser.py @@ -26,18 +26,19 @@ from pymodbus.transaction import ModbusAsciiFramer from pymodbus.transaction import ModbusRtuFramer from pymodbus.compat import IS_PYTHON3 - - # -------------------------------------------------------------------------- # -# Logging # -------------------------------------------------------------------------- # 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): @@ -62,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__) @@ -71,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: @@ -83,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 @@ -146,10 +149,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) - (opt, arg) = parser.parse_args() if not opt.message and len(arg) > 0: diff --git a/examples/contrib/modbus_simulator.py b/examples/contrib/modbus_simulator.py index 09ae0c41f..6f9e4253a 100644 --- a/examples/contrib/modbus_simulator.py +++ b/examples/contrib/modbus_simulator.py @@ -76,8 +76,9 @@ def __init__(self, config): :param config: The pickled datastore """ try: - self.file = open(config, "r") - except Exception: + self.file = open(config, "rb") + except Exception as e: + _logger.critical(str(e)) raise ConfigurationException("File not found %s" % config) def parse(self): @@ -134,5 +135,5 @@ def main(): if __name__ == "__main__": if root_test(): main() - else: + else: print("This script must be run as root!") diff --git a/examples/contrib/remote_server_context.py b/examples/contrib/remote_server_context.py index c21a4632f..b8af75e4a 100644 --- a/examples/contrib/remote_server_context.py +++ b/examples/contrib/remote_server_context.py @@ -70,7 +70,7 @@ def validate(self, fx, address, count=1): result = self.context.get_callbacks[self.decode(fx)](address, count, self.unit_id) - return result.function_code < 0x80 + return not result.isError() def getValues(self, fx, address, count=1): """ Validates the request to make sure it is in range @@ -113,7 +113,7 @@ def __extract_result(self, fx, result): :param fx: The function to call :param result: The resulting data """ - if result.function_code < 0x80: + if not result.isError(): if fx in ['d', 'c']: return result.bits if fx in ['h', 'i']: diff --git a/examples/contrib/serial_forwarder.py b/examples/contrib/serial_forwarder.py index de478050c..ece9011f9 100755 --- a/examples/contrib/serial_forwarder.py +++ b/examples/contrib/serial_forwarder.py @@ -29,14 +29,14 @@ def run_serial_forwarder(): # ----------------------------------------------------------------------- # # initialize the datastore(serial client) # ----------------------------------------------------------------------- # - client = ModbusClient(method='ascii', port='/dev/pts/14') + client = ModbusClient(method='rtu', port='/dev/ptyp0') store = RemoteSlaveContext(client) context = ModbusServerContext(slaves=store, single=True) # ----------------------------------------------------------------------- # # run the server you want # ----------------------------------------------------------------------- # - StartServer(context) + StartServer(context, address=("localhost", 5020)) if __name__ == "__main__": diff --git a/examples/functional/base_runner.py b/examples/functional/base_runner.py index 34e81d262..9b84b46e6 100644 --- a/examples/functional/base_runner.py +++ b/examples/functional/base_runner.py @@ -3,9 +3,9 @@ from subprocess import Popen as execute from twisted.internet.defer import Deferred -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # # configure the client logging -# --------------------------------------------------------------------------- # +# --------------------------------------------------------------------------- # import logging log = logging.getLogger(__name__) @@ -30,33 +30,33 @@ def shutdown(self): def testReadWriteCoil(self): rq = self.client.write_coil(1, True) rr = self.client.read_coils(1,1) - self.__validate(rq, lambda r: r.function_code < 0x80) - self.__validate(rr, lambda r: r.bits[0] == True) - + self._validate(rq, lambda r: not r.isError()) + self._validate(rr, lambda r: r.bits[0] == True) + def testReadWriteCoils(self): rq = self.client.write_coils(1, [True]*8) rr = self.client.read_coils(1,8) - self.__validate(rq, lambda r: r.function_code < 0x80) - self.__validate(rr, lambda r: r.bits == [True]*8) - + self._validate(rq, lambda r: not r.isError()) + self._validate(rr, lambda r: r.bits == [True]*8) + def testReadWriteDiscreteRegisters(self): rq = self.client.write_coils(1, [False]*8) rr = self.client.read_discrete_inputs(1,8) - self.__validate(rq, lambda r: r.function_code < 0x80) - self.__validate(rr, lambda r: r.bits == [False]*8) - + self._validate(rq, lambda r: not r.isError()) + self._validate(rr, lambda r: r.bits == [False]*8) + def testReadWriteHoldingRegisters(self): rq = self.client.write_register(1, 10) rr = self.client.read_holding_registers(1,1) - self.__validate(rq, lambda r: r.function_code < 0x80) - self.__validate(rr, lambda r: r.registers[0] == 10) - + self._validate(rq, lambda r: not r.isError()) + self._validate(rr, lambda r: r.registers[0] == 10) + def testReadWriteInputRegisters(self): rq = self.client.write_registers(1, [10]*8) rr = self.client.read_input_registers(1,8) - self.__validate(rq, lambda r: r.function_code < 0x80) - self.__validate(rr, lambda r: r.registers == [10]*8) - + self._validate(rq, lambda r: not r.isError()) + self._validate(rr, lambda r: r.registers == [10]*8) + def testReadWriteRegistersTogether(self): arguments = { 'read_address': 1, @@ -66,14 +66,14 @@ def testReadWriteRegistersTogether(self): } rq = self.client.readwrite_registers(**arguments) rr = self.client.read_input_registers(1,8) - self.__validate(rq, lambda r: r.function_code < 0x80) - self.__validate(rr, lambda r: r.registers == [20]*8) + self._validate(rq, lambda r: not r.isError()) + self._validate(rr, lambda r: r.registers == [20]*8) - def __validate(self, result, test): + def _validate(self, result, test): """ Validate the result whether it is a result or a deferred. - :param result: The result to __validate - :param callback: The test to __validate + :param result: The result to _validate + :param callback: The test to _validate """ if isinstance(result, Deferred): deferred.callback(lambda : self.assertTrue(test(result))) 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/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 4e2f4bde3..ea3c981c3 100644 --- a/pymodbus/client/common.py +++ b/pymodbus/client/common.py @@ -14,6 +14,8 @@ from pymodbus.file_message import * from pymodbus.other_message import * +from pymodbus.utilities import ModbusTransactionState + class ModbusClientMixin(object): ''' @@ -30,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 80946b77a..f1642aee7 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -1,10 +1,11 @@ import socket 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 -from pymodbus.compat import byte2int from pymodbus.exceptions import NotImplementedException, ParameterException from pymodbus.exceptions import ConnectionException from pymodbus.transaction import FifoTransactionManager @@ -13,111 +14,162 @@ from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer from pymodbus.client.common import ModbusClientMixin -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Logging -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # import logging _logger = logging.getLogger(__name__) - -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # 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) + self._debug = False + self._debugfd = None - #-----------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # 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 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 + """ 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): + return self._recv(size) + 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 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 + """ 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 +178,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) - BaseModbusClient.__init__(self, framer(ClientDecoder()), **kwargs) + self.timeout = kwargs.get('timeout', Defaults.Timeout) + BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **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 +223,84 @@ 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) + # 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 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) + """ + return "ModbusTcpClient(%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) + BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **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 +308,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 +321,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 +341,36 @@ 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 is_socket_open(self): + return True if self.socket is not None else False + 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) + """ + return "ModbusUdpClient(%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,64 +385,84 @@ 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, self), + **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) + self.last_frame_end = None if self.method == "rtu": - self._last_frame_end = 0.0 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 = 3.5 * (1 + 8 + 2) / self.baudrate + self.silent_interval = round(self.silent_interval, 6) @staticmethod - def __implementation(method): - ''' Returns the requested framer + def __implementation(method, client): + """ 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(), client) + elif method == 'rtu': + return ModbusRtuFramer(ClientDecoder(), client) + elif method == 'binary': + return ModbusBinaryFramer(ClientDecoder(), client) + elif method == 'socket': + return ModbusSocketFramer(ClientDecoder(), client) 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 + """ + 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 _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 + """ Sends data on the underlying socket If receive buffer still holds some data then flush it. @@ -361,58 +471,67 @@ def _send(self, request): :param request: The encoded request to send :return: The number of bytes written - ''' + """ 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) - 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): - _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) - if self.method == "rtu": - self._last_frame_end = time.time() 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 + """ 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__()) + if size is None: + size = self._wait_for_data() result = self.socket.read(size) - if self.method == "rtu": - self._last_frame_end = time.time() 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 + """ Builds a string representation of the connection :returns: The string representation - ''' - return "%s baud[%s]" % (self.method, self.baudrate) + """ + return "ModbusSerialClient(%s baud[%s])" % (self.method, self.baudrate) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Exported symbols -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # + + __all__ = [ "ModbusTcpClient", "ModbusUdpClient", "ModbusSerialClient" -] \ No newline at end of file +] 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/datastore/context.py b/pymodbus/datastore/context.py index a5e311a34..b21d21d42 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,14 @@ 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)) + + def slaves(self): + # Python3 now returns keys() as iterable + return list(self._slaves.keys()) diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index 0183f8919..3f6726bd8 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -41,7 +41,7 @@ def validate(self, fx, address, count=1): ''' _logger.debug("validate[%d] %d:%d" % (fx, address, count)) result = self.__get_callbacks[self.decode(fx)](address, count) - return result.function_code < 0x80 + return not result.isError() def getValues(self, fx, address, count=1): ''' Validates the request to make sure it is in range @@ -99,7 +99,7 @@ def __extract_result(self, fx, result): ''' A helper method to extract the values out of a response. TODO make this consistent (values?) ''' - if result.function_code < 0x80: + if not result.isError(): if fx in ['d', 'c']: return result.bits if fx in ['h', 'i']: return result.registers else: return result 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/__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..21c932842 --- /dev/null +++ b/pymodbus/framer/rtu_framer.py @@ -0,0 +1,314 @@ +import struct +import time + +from pymodbus.exceptions import ModbusIOException +from pymodbus.exceptions import InvalidMessageReceivedException +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) + 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 - [{}] not ready".format(data)) + + 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.TRANSACTION_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) + 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 InvalidMessageReceivedException(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..201018960 --- /dev/null +++ b/pymodbus/framer/socket_framer.py @@ -0,0 +1,217 @@ +import struct +from pymodbus.exceptions import ModbusIOException +from pymodbus.exceptions import InvalidMessageReceivedException +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 InvalidMessageReceivedException(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/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/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/pdu.py b/pymodbus/pdu.py index 166f362c1..d70751df2 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -1,24 +1,24 @@ -''' +""" Contains base classes for modbus request/response/error packets -''' +""" from pymodbus.interfaces import Singleton from pymodbus.exceptions import NotImplementedException from pymodbus.constants import Defaults from pymodbus.utilities import rtuFrameSize from pymodbus.compat import iteritems, int2byte, byte2int -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Logging -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # import logging _logger = logging.getLogger(__name__) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Base PDU's -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusPDU(object): - ''' + """ Base class for all Modbus mesages .. attribute:: transaction_id @@ -50,10 +50,10 @@ class ModbusPDU(object): to create a complicated message. By setting this to True, the request will pass the currently encoded message through instead of encoding it again. - ''' + """ def __init__(self, **kwargs): - ''' Initializes the base data for a modbus request ''' + """ Initializes the base data for a modbus request """ self.transaction_id = kwargs.get('transaction', Defaults.TransactionId) self.protocol_id = kwargs.get('protocol', Defaults.ProtocolId) self.unit_id = kwargs.get('unit', Defaults.UnitId) @@ -61,27 +61,27 @@ def __init__(self, **kwargs): self.check = 0x0000 def encode(self): - ''' Encodes the message + """ Encodes the message :raises: A not implemented exception - ''' + """ raise NotImplementedException() def decode(self, data): - ''' Decodes data part of the message. + """ Decodes data part of the message. :param data: is a string object :raises: A not implemented exception - ''' + """ raise NotImplementedException() @classmethod def calculateRtuFrameSize(cls, buffer): - ''' Calculates the size of a PDU. + """ Calculates the size of a PDU. :param buffer: A buffer containing the data that have been received. :returns: The number of bytes in the PDU. - ''' + """ if hasattr(cls, '_rtu_frame_size'): return cls._rtu_frame_size elif hasattr(cls, '_rtu_byte_count_pos'): @@ -91,25 +91,25 @@ def calculateRtuFrameSize(cls, buffer): class ModbusRequest(ModbusPDU): - ''' Base class for a modbus request PDU ''' + """ Base class for a modbus request PDU """ def __init__(self, **kwargs): - ''' Proxy to the lower level initializer ''' + """ Proxy to the lower level initializer """ ModbusPDU.__init__(self, **kwargs) def doException(self, exception): - ''' Builds an error response based on the function + """ Builds an error response based on the function :param exception: The exception to return :raises: An exception response - ''' + """ _logger.error("Exception Response F(%d) E(%d)" % (self.function_code, exception)) return ExceptionResponse(self.function_code, exception) class ModbusResponse(ModbusPDU): - ''' Base class for a modbus response PDU + """ Base class for a modbus response PDU .. attribute:: should_respond @@ -120,22 +120,25 @@ class ModbusResponse(ModbusPDU): Indicates the size of the modbus rtu response used for calculating how much to read. - ''' + """ should_respond = True def __init__(self, **kwargs): - ''' Proxy to the lower level initializer ''' + """ Proxy to the lower level initializer """ ModbusPDU.__init__(self, **kwargs) + def isError(self): + return self.function_code > 0x80 -#---------------------------------------------------------------------------# + +# --------------------------------------------------------------------------- # # Exception PDU's -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # class ModbusExceptions(Singleton): - ''' + """ An enumeration of the valid modbus exceptions - ''' + """ IllegalFunction = 0x01 IllegalAddress = 0x02 IllegalValue = 0x03 @@ -148,93 +151,96 @@ class ModbusExceptions(Singleton): @classmethod def decode(cls, code): - ''' Given an error code, translate it to a + """ Given an error code, translate it to a string error name. :param code: The code number to translate - ''' + """ values = dict((v, k) for k, v in iteritems(cls.__dict__) if not k.startswith('__') and not callable(v)) return values.get(code, None) class ExceptionResponse(ModbusResponse): - ''' Base class for a modbus exception PDU ''' + """ Base class for a modbus exception PDU """ ExceptionOffset = 0x80 _rtu_frame_size = 5 def __init__(self, function_code, exception_code=None, **kwargs): - ''' Initializes the modbus exception response + """ Initializes the modbus exception response :param function_code: The function to build an exception response for :param exception_code: The specific modbus exception to return - ''' + """ ModbusResponse.__init__(self, **kwargs) self.original_code = function_code self.function_code = function_code | self.ExceptionOffset self.exception_code = exception_code def encode(self): - ''' Encodes a modbus exception response + """ Encodes a modbus exception response :returns: The encoded exception packet - ''' + """ return int2byte(self.exception_code) def decode(self, data): - ''' Decodes a modbus exception response + """ Decodes a modbus exception response :param data: The packet data to decode - ''' + """ self.exception_code = byte2int(data[0]) def __str__(self): - ''' Builds a representation of an exception response + """ Builds a representation of an exception response :returns: The string representation of an exception response - ''' + """ message = ModbusExceptions.decode(self.exception_code) parameters = (self.function_code, self.original_code, message) return "Exception Response(%d, %d, %s)" % parameters class IllegalFunctionRequest(ModbusRequest): - ''' + """ Defines the Modbus slave exception type 'Illegal Function' This exception code is returned if the slave:: - does not implement the function code **or** - is not in a state that allows it to process the function - ''' + """ ErrorCode = 1 def __init__(self, function_code, **kwargs): - ''' Initializes a IllegalFunctionRequest + """ Initializes a IllegalFunctionRequest :param function_code: The function we are erroring on - ''' + """ ModbusRequest.__init__(self, **kwargs) self.function_code = function_code def decode(self, data): - ''' This is here so this failure will run correctly + """ This is here so this failure will run correctly :param data: Not used - ''' + """ pass def execute(self, context): - ''' Builds an illegal function request error response + """ Builds an illegal function request error response :param context: The current context for the message :returns: The error response packet - ''' + """ return ExceptionResponse(self.function_code, self.ErrorCode) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Exported symbols -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # + + __all__ = [ 'ModbusRequest', 'ModbusResponse', 'ModbusExceptions', 'ExceptionResponse', 'IllegalFunctionRequest', ] + 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/server/async.py b/pymodbus/server/async.py index 5dfdc1f65..3e52586db 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -1,73 +1,79 @@ -''' +""" Implementation of a Twisted Modbus Server ------------------------------------------ -''' +""" from binascii import b2a_hex from twisted.internet import protocol from twisted.internet.protocol import ServerFactory 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 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(' '.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]) - 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) @@ -79,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) @@ -98,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. @@ -117,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() @@ -129,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. @@ -144,40 +150,43 @@ 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(" ".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) 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) @@ -190,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): @@ -202,9 +211,9 @@ def _send(self, message, addr): return self.transport.write(pdu, addr) -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # # Starting Factories -#---------------------------------------------------------------------------# +# --------------------------------------------------------------------------- # def _is_main_thread(): import threading @@ -220,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) @@ -271,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 @@ -286,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(): @@ -304,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 7941701d6..cb7c26e39 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,50 +19,52 @@ 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.framer = self.server.framer(self.server.decoder, client=None) 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) 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() ) @@ -70,60 +73,52 @@ 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])) - if isinstance(self.framer, ModbusAsciiFramer): - unit_address = int(data[1:3], 16) - elif isinstance(self.framer, ModbusBinaryFramer): - unit_address = byte2int(data[1]) - else: - unit_address = byte2int(data[0]) - - 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 + """ 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,34 +136,43 @@ 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 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) + + 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) @@ -186,12 +190,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 +203,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,9 +222,12 @@ 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) + 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) @@ -231,15 +238,17 @@ 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 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 +257,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,45 +280,49 @@ 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) 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 + """ 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 +330,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,37 +349,41 @@ 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) 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 + """ 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 +391,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 +416,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 +473,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 +485,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 +499,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 + """ + framer = kwargs.pop("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 +542,19 @@ 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 24756f19f..9049c5296 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -1,29 +1,38 @@ ''' 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 functools import partial + 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.compat import iterkeys, imap, byte2int - -#---------------------------------------------------------------------------# +from pymodbus.exceptions import InvalidMessageReceivedException +from pymodbus.constants import Defaults +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: + TimeoutError +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 @@ -50,7 +59,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() @@ -86,114 +97,188 @@ def _calculate_exception_length(self): return None - def _check_response(self, response): - ''' Checks if the response is a Modbus Exception. - ''' - if isinstance(self.client.framer, ModbusSocketFramer): - if len(response) >= 8 and byte2int(response[7]) > 128: - return False - 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)): - if len(response) >= 2 and byte2int(response[1]) > 128: - return False - - return True - 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: + with self._transaction_lock: 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) - else: - last_exception = last_exception or ("No Response " - "received from the remote unit") - response = ModbusIOException(last_exception) - - return response + _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) + _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: + full = False + c_str = str(self.client) + if "modbusudpclient" in c_str.lower().strip(): + full = True + if not expected_response_length: + expected_response_length = Defaults.ReadSize + 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 and response: + 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 + # Remove entry + self._no_response_devices.remove(request.unit_id) + break + addTransaction = partial(self.addTransaction, + tid=request.transaction_id) + self.client.framer.processIncomingPacket(response, + 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" + "/Unable to decode response") + response = ModbusIOException(last_exception) + if hasattr(self.client, "state"): + _logger.debug("Changing transaction state from " + "'PROCESSING REPLY' to " + "'TRANSACTION_COMPLETE'") + self.client.state = ( + ModbusTransactionState.TRANSACTION_COMPLETE) + return response + except ModbusIOException as ex: + # Handle decode errors in processIncomingPacket method + _logger.exception(ex) + self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE + return ex + + 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)) + 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, full) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug("RECV: " + hexlify_packets(result)) + except (socket.error, ModbusIOException, + InvalidMessageReceivedException) 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 + return self.client.framer.sendPacket(packet) + + def _recv(self, expected_response_length, full): + total = None + 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.framer.recvPacket(min_size) + if len(read_min) != min_size: + raise InvalidMessageReceivedException( + "Incomplete message received, expected at least %d bytes " + "(%d received)" % (min_size, len(read_min)) + ) + 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, 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 + 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 + else: + total = expected_response_length + else: + read_min = b'' + total = expected_response_length + result = self.client.framer.recvPacket(expected_response_length) + result = read_min + result + actual = len(result) + if total is not None and 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): @@ -270,7 +355,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): @@ -280,7 +365,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): @@ -288,7 +373,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) @@ -322,7 +407,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): @@ -332,7 +417,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): @@ -340,754 +425,13 @@ 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) -#---------------------------------------------------------------------------# -# 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(' '.join([hex(byte2int(x)) for x in 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() - 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). - ''' - 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: return buffer - return '' - - 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 - """ - 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]) - 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 20ef72663..dff3f10b2 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 + TRANSACTION_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.TRANSACTION_COMPLETE: "TRANSCATION_COMPLETE" + } + return states.get(state, None) + + # --------------------------------------------------------------------------- # # Helpers # --------------------------------------------------------------------------- # + def default(value): """ Given a python object, return the default value @@ -205,6 +233,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/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/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/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 516b44298..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()) @@ -91,12 +106,13 @@ 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''' 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()) @@ -158,7 +176,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''' @@ -187,27 +205,45 @@ 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) - 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") def testBasicSyncSerialClient(self, mock_serial): @@ -217,12 +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)) - self.assertEqual(1, client._send('\x00')) - self.assertEqual('\x00', client._recv(1)) + client.state = 0 + self.assertEqual(1, client._send(b'\x00')) + self.assertEqual(b'\x00', client._recv(1)) # connect/disconnect self.assertTrue(client.connect()) @@ -232,7 +270,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''' @@ -255,20 +293,24 @@ 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") 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)) # 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): @@ -277,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_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 8fbd9758f..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[0])] + 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) @@ -100,7 +102,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..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 @@ -108,17 +107,18 @@ 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 # 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) # 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 @@ -147,6 +147,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() @@ -158,7 +161,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 @@ -181,7 +184,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 @@ -190,6 +193,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() @@ -205,6 +209,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) @@ -239,7 +244,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 diff --git a/test/test_transaction.py b/test/test_transaction.py index 2627bd380..85e00223c 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +import pytest import unittest from binascii import a2b_hex from pymodbus.pdu import * @@ -11,48 +12,131 @@ from pymodbus.compat import byte2int from mock import MagicMock from pymodbus.exceptions import ( - NotImplementedException, ModbusIOException, InvalidMessageRecievedException + NotImplementedException, ModbusIOException, InvalidMessageReceivedException ) + class ModbusTransactionTest(unittest.TestCase): - ''' + """ This is the unittest for the pymodbus.transaction module - ''' + """ - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # Test Construction - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def setUp(self): - ''' Sets up the test environment ''' + """ 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) def tearDown(self): - ''' Cleans up the test environment ''' + """ Cleans up the test environment """ del self._manager del self._tcp del self._rtu del self._ascii - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # + # Base transaction manager + # ----------------------------------------------------------------------- # + + def testCalculateExpectedResponseLength(self): + self._tm.client = MagicMock() + self._tm.client.framer = MagicMock() + self._tm._set_adu_size() + self.assertEqual(self._tm._calculate_response_length(0), None) + self._tm.base_adu_size = 10 + self.assertEqual(self._tm._calculate_response_length(5), 15) + + def testCalculateExceptionLength(self): + for framer, exception_length in [('ascii', 11), + ('binary', 7), + ('rtu', 5), + ('tcp', 9), + ('dummy', None)]: + self._tm.client = MagicMock() + if framer == "ascii": + self._tm.client.framer = self._ascii + elif framer == "binary": + self._tm.client.framer = self._binary + elif framer == "rtu": + self._tm.client.framer = self._rtu + elif framer == "tcp": + self._tm.client.framer = self._tcp + else: + self._tm.client.framer = MagicMock() + + self._tm._set_adu_size() + self.assertEqual(self._tm._calculate_exception_length(), + exception_length) + + def testExecute(self): + client = MagicMock() + client.framer = self._ascii + client.framer._buffer = b'deadbeef' + client.framer.processIncomingPacket = MagicMock() + client.framer.processIncomingPacket.return_value = None + client.framer.buildPacket = MagicMock() + client.framer.buildPacket.return_value = b'deadbeef' + client.framer.sendPacket = MagicMock() + client.framer.sendPacket.return_value = len(b'deadbeef') + + request = MagicMock() + request.get_response_pdu_size.return_value = 10 + request.unit_id = 1 + tm = ModbusTransactionManager(client) + tm._recv = MagicMock(return_value=b'abcdef') + self.assertEqual(tm.retries, 3) + self.assertEqual(tm.retry_on_empty, False) + # tm._transact = MagicMock() + # some response + # tm._transact.return_value = (b'abcdef', None) + tm.getTransaction = MagicMock() + tm.getTransaction.return_value = 'response' + response = tm.execute(request) + self.assertEqual(response, 'response') + # No response + tm._recv = MagicMock(return_value=b'abcdef') + # tm._transact.return_value = (b'', None) + tm.transactions = [] + tm.getTransaction = MagicMock() + tm.getTransaction.return_value = None + response = tm.execute(request) + self.assertIsInstance(response, ModbusIOException) + + # No response with retries + tm.retry_on_empty = True + tm._recv = MagicMock(side_effect=iter([b'', b'abcdef'])) + # tm._transact.side_effect = [(b'', None), (b'abcdef', None)] + response = tm.execute(request) + self.assertIsInstance(response, ModbusIOException) + + # Unable to decode response + tm._recv = MagicMock(side_effect=ModbusIOException()) + # tm._transact.side_effect = [(b'abcdef', None)] + client.framer.processIncomingPacket.side_effect = MagicMock(side_effect=ModbusIOException()) + self.assertIsInstance(tm.execute(request), ModbusIOException) + + # ----------------------------------------------------------------------- # # Dictionary based transaction manager - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # + def testDictTransactionManagerTID(self): - ''' Test the dict transaction manager TID ''' + """ Test the dict transaction manager TID """ for tid in range(1, self._manager.getNextTID() + 10): self.assertEqual(tid+1, self._manager.getNextTID()) self._manager.reset() self.assertEqual(1, self._manager.getNextTID()) def testGetDictTransactionManagerTransaction(self): - ''' Test the dict transaction manager ''' + """ Test the dict transaction manager """ class Request: pass self._manager.reset() handle = Request() @@ -63,7 +147,7 @@ class Request: pass self.assertEqual(handle.message, result.message) def testDeleteDictTransactionManagerTransaction(self): - ''' Test the dict transaction manager ''' + """ Test the dict transaction manager """ class Request: pass self._manager.reset() handle = Request() @@ -74,18 +158,18 @@ class Request: pass self._manager.delTransaction(handle.transaction_id) self.assertEqual(None, self._manager.getTransaction(handle.transaction_id)) - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # Queue based transaction manager - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def testFifoTransactionManagerTID(self): - ''' Test the fifo transaction manager TID ''' + """ Test the fifo transaction manager TID """ for tid in range(1, self._queue_manager.getNextTID() + 10): self.assertEqual(tid+1, self._queue_manager.getNextTID()) self._queue_manager.reset() self.assertEqual(1, self._queue_manager.getNextTID()) def testGetFifoTransactionManagerTransaction(self): - ''' Test the fifo transaction manager ''' + """ Test the fifo transaction manager """ class Request: pass self._queue_manager.reset() handle = Request() @@ -96,7 +180,7 @@ class Request: pass self.assertEqual(handle.message, result.message) def testDeleteFifoTransactionManagerTransaction(self): - ''' Test the fifo transaction manager ''' + """ Test the fifo transaction manager """ class Request: pass self._queue_manager.reset() handle = Request() @@ -107,11 +191,11 @@ class Request: pass self._queue_manager.delTransaction(handle.transaction_id) self.assertEqual(None, self._queue_manager.getTransaction(handle.transaction_id)) - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # TCP tests - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def testTCPFramerTransactionReady(self): - ''' Test a tcp frame transaction ''' + """ Test a tcp frame transaction """ msg = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" self.assertFalse(self._tcp.isFrameReady()) self.assertFalse(self._tcp.checkFrame()) @@ -124,7 +208,7 @@ def testTCPFramerTransactionReady(self): self.assertEqual(b'', self._ascii.getFrame()) def testTCPFramerTransactionFull(self): - ''' Test a full tcp frame transaction ''' + """ Test a full tcp frame transaction """ msg = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" self._tcp.addToFrame(msg) self.assertTrue(self._tcp.checkFrame()) @@ -133,7 +217,7 @@ def testTCPFramerTransactionFull(self): self._tcp.advanceFrame() def testTCPFramerTransactionHalf(self): - ''' Test a half completed tcp frame transaction ''' + """ Test a half completed tcp frame transaction """ msg1 = b"\x00\x01\x12\x34\x00" msg2 = b"\x04\xff\x02\x12\x34" self._tcp.addToFrame(msg1) @@ -147,7 +231,7 @@ def testTCPFramerTransactionHalf(self): self._tcp.advanceFrame() def testTCPFramerTransactionHalf2(self): - ''' Test a half completed tcp frame transaction ''' + """ Test a half completed tcp frame transaction """ msg1 = b"\x00\x01\x12\x34\x00\x04\xff" msg2 = b"\x02\x12\x34" self._tcp.addToFrame(msg1) @@ -161,7 +245,7 @@ def testTCPFramerTransactionHalf2(self): self._tcp.advanceFrame() def testTCPFramerTransactionHalf3(self): - ''' Test a half completed tcp frame transaction ''' + """ Test a half completed tcp frame transaction """ msg1 = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12" msg2 = b"\x34" self._tcp.addToFrame(msg1) @@ -175,7 +259,7 @@ def testTCPFramerTransactionHalf3(self): self._tcp.advanceFrame() def testTCPFramerTransactionShort(self): - ''' Test that we can get back on track after an invalid message ''' + """ Test that we can get back on track after an invalid message """ msg1 = b"\x99\x99\x99\x99\x00\x01\x00\x01" msg2 = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" self._tcp.addToFrame(msg1) @@ -191,7 +275,7 @@ def testTCPFramerTransactionShort(self): self._tcp.advanceFrame() def testTCPFramerPopulate(self): - ''' Test a tcp frame packet build ''' + """ Test a tcp frame packet build """ expected = ModbusRequest() expected.transaction_id = 0x0001 expected.protocol_id = 0x1234 @@ -206,7 +290,7 @@ def testTCPFramerPopulate(self): self._tcp.advanceFrame() def testTCPFramerPacket(self): - ''' Test a tcp frame packet build ''' + """ Test a tcp frame packet build """ old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b'' message = ModbusRequest() @@ -219,11 +303,11 @@ def testTCPFramerPacket(self): self.assertEqual(expected, actual) ModbusRequest.encode = old_encode - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # RTU tests - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def testRTUFramerTransactionReady(self): - ''' Test if the checks for a complete frame work ''' + """ Test if the checks for a complete frame work """ self.assertFalse(self._rtu.isFrameReady()) msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] @@ -236,7 +320,7 @@ def testRTUFramerTransactionReady(self): self.assertTrue(self._rtu.checkFrame()) def testRTUFramerTransactionFull(self): - ''' Test a full rtu frame transaction ''' + """ Test a full rtu frame transaction """ msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" stripped_msg = msg[1:-2] self._rtu.addToFrame(msg) @@ -246,7 +330,7 @@ def testRTUFramerTransactionFull(self): self._rtu.advanceFrame() def testRTUFramerTransactionHalf(self): - ''' Test a half completed rtu frame transaction ''' + """ Test a half completed rtu frame transaction """ msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] stripped_msg = b"".join(msg_parts)[1:-2] self._rtu.addToFrame(msg_parts[0]) @@ -259,7 +343,7 @@ def testRTUFramerTransactionHalf(self): self._rtu.advanceFrame() def testRTUFramerPopulate(self): - ''' Test a rtu frame packet build ''' + """ Test a rtu frame packet build """ request = ModbusRequest() msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" self._rtu.addToFrame(msg) @@ -274,7 +358,7 @@ def testRTUFramerPopulate(self): self.assertEqual(0x00, request.unit_id) def testRTUFramerPacket(self): - ''' Test a rtu frame packet build ''' + """ Test a rtu frame packet build """ old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b'' message = ModbusRequest() @@ -286,7 +370,7 @@ def testRTUFramerPacket(self): ModbusRequest.encode = old_encode def testRTUDecodeException(self): - ''' Test that the RTU framer can decode errors ''' + """ Test that the RTU framer can decode errors """ message = b"\x00\x90\x02\x9c\x01" actual = self._rtu.addToFrame(message) result = self._rtu.checkFrame() @@ -318,7 +402,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,13 +411,13 @@ 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 - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def testASCIIFramerTransactionReady(self): - ''' Test a ascii frame transaction ''' + """ Test a ascii frame transaction """ msg = b':F7031389000A60\r\n' self.assertFalse(self._ascii.isFrameReady()) self.assertFalse(self._ascii.checkFrame()) @@ -346,7 +430,7 @@ def testASCIIFramerTransactionReady(self): self.assertEqual(b'', self._ascii.getFrame()) def testASCIIFramerTransactionFull(self): - ''' Test a full ascii frame transaction ''' + """ Test a full ascii frame transaction """ msg = b'sss:F7031389000A60\r\n' pack = a2b_hex(msg[6:-4]) self._ascii.addToFrame(msg) @@ -356,7 +440,7 @@ def testASCIIFramerTransactionFull(self): self._ascii.advanceFrame() def testASCIIFramerTransactionHalf(self): - ''' Test a half completed ascii frame transaction ''' + """ Test a half completed ascii frame transaction """ msg1 = b'sss:F7031389' msg2 = b'000A60\r\n' pack = a2b_hex(msg1[6:] + msg2[:-4]) @@ -371,13 +455,13 @@ def testASCIIFramerTransactionHalf(self): self._ascii.advanceFrame() def testASCIIFramerPopulate(self): - ''' Test a ascii frame packet build ''' + """ Test a ascii frame packet build """ request = ModbusRequest() self._ascii.populateResult(request) self.assertEqual(0x00, request.unit_id) def testASCIIFramerPacket(self): - ''' Test a ascii frame packet build ''' + """ Test a ascii frame packet build """ old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b'' message = ModbusRequest() @@ -390,22 +474,21 @@ 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) - - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # # Binary tests - #---------------------------------------------------------------------------# + # ----------------------------------------------------------------------- # def testBinaryFramerTransactionReady(self): - ''' Test a binary frame transaction ''' + """ Test a binary frame transaction """ msg = b'\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d' self.assertFalse(self._binary.isFrameReady()) self.assertFalse(self._binary.checkFrame()) @@ -418,7 +501,7 @@ def testBinaryFramerTransactionReady(self): self.assertEqual(b'', self._binary.getFrame()) def testBinaryFramerTransactionFull(self): - ''' Test a full binary frame transaction ''' + """ Test a full binary frame transaction """ msg = b'\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d' pack = msg[2:-3] self._binary.addToFrame(msg) @@ -428,7 +511,7 @@ def testBinaryFramerTransactionFull(self): self._binary.advanceFrame() def testBinaryFramerTransactionHalf(self): - ''' Test a half completed binary frame transaction ''' + """ Test a half completed binary frame transaction """ msg1 = b'\x7b\x01\x03\x00' msg2 = b'\x00\x00\x05\x85\xC9\x7d' pack = msg1[2:] + msg2[:-3] @@ -443,13 +526,13 @@ def testBinaryFramerTransactionHalf(self): self._binary.advanceFrame() def testBinaryFramerPopulate(self): - ''' Test a binary frame packet build ''' + """ Test a binary frame packet build """ request = ModbusRequest() self._binary.populateResult(request) self.assertEqual(0x00, request.unit_id) def testBinaryFramerPacket(self): - ''' Test a binary frame packet build ''' + """ Test a binary frame packet build """ old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b'' message = ModbusRequest() @@ -462,18 +545,20 @@ 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 -#---------------------------------------------------------------------------# +# ----------------------------------------------------------------------- # + + if __name__ == "__main__": - unittest.main() + pytest.main() diff --git a/tox.ini b/tox.ini index ed6f94fd5..5252c87fa 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist = py27, py34, py35, py36, pypy [testenv] deps = -requirements-tests.txt -commands = nostests {posargs} +commands = py.test {posargs} [flake8] exclude = .tox